532 lines
10 KiB
C
532 lines
10 KiB
C
/* $OpenBSD: smtpc.c,v 1.19 2021/07/14 13:33:57 kn Exp $ */
|
|
|
|
/*
|
|
* Copyright (c) 2018 Eric Faurot <eric@openbsd.org>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
#include <sys/socket.h>
|
|
|
|
#include <event.h>
|
|
#include <netdb.h>
|
|
#include <pwd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <syslog.h>
|
|
#include <tls.h>
|
|
#include <unistd.h>
|
|
|
|
#include "smtp.h"
|
|
#include "log.h"
|
|
|
|
static void parse_server(char *);
|
|
static void parse_message(FILE *);
|
|
static void resume(void);
|
|
|
|
static int verbose = 1;
|
|
static int done = 0;
|
|
static int noaction = 0;
|
|
static struct addrinfo *res0, *ai;
|
|
static struct smtp_params params;
|
|
static struct smtp_mail mail;
|
|
static const char *servname = NULL;
|
|
static struct tls_config *tls_config;
|
|
|
|
static int nosni = 0;
|
|
static const char *cafile = NULL;
|
|
static const char *protocols = NULL;
|
|
static const char *ciphers = NULL;
|
|
|
|
static void
|
|
usage(void)
|
|
{
|
|
extern char *__progname;
|
|
|
|
fprintf(stderr, "usage: %s [-Chnv] [-a authfile] [-F from] [-H helo] "
|
|
"[-s server] [-T params] [recipient ...]\n", __progname);
|
|
exit(1);
|
|
}
|
|
|
|
static void
|
|
parse_tls_options(char *opt)
|
|
{
|
|
static char * const tokens[] = {
|
|
#define CAFILE 0
|
|
"cafile",
|
|
#define CIPHERS 1
|
|
"ciphers",
|
|
#define NOSNI 2
|
|
"nosni",
|
|
#define NOVERIFY 3
|
|
"noverify",
|
|
#define PROTOCOLS 4
|
|
"protocols",
|
|
#define SERVERNAME 5
|
|
"servername",
|
|
NULL };
|
|
char *value;
|
|
|
|
while (*opt) {
|
|
switch (getsubopt(&opt, tokens, &value)) {
|
|
case CAFILE:
|
|
if (value == NULL)
|
|
fatalx("missing value for cafile");
|
|
cafile = value;
|
|
break;
|
|
case CIPHERS:
|
|
if (value == NULL)
|
|
fatalx("missing value for ciphers");
|
|
ciphers = value;
|
|
break;
|
|
case NOSNI:
|
|
if (value != NULL)
|
|
fatalx("no value expected for nosni");
|
|
nosni = 1;
|
|
break;
|
|
case NOVERIFY:
|
|
if (value != NULL)
|
|
fatalx("no value expected for noverify");
|
|
params.tls_verify = 0;
|
|
break;
|
|
case PROTOCOLS:
|
|
if (value == NULL)
|
|
fatalx("missing value for protocols");
|
|
protocols = value;
|
|
break;
|
|
case SERVERNAME:
|
|
if (value == NULL)
|
|
fatalx("missing value for servername");
|
|
servname = value;
|
|
break;
|
|
case -1:
|
|
if (suboptarg)
|
|
fatalx("invalid TLS option \"%s\"", suboptarg);
|
|
fatalx("missing TLS option");
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
char hostname[256];
|
|
FILE *authfile;
|
|
int ch, i;
|
|
uint32_t protos;
|
|
char *server = "localhost";
|
|
char *authstr = NULL;
|
|
size_t alloc = 0;
|
|
ssize_t len;
|
|
struct passwd *pw;
|
|
|
|
log_init(1, 0);
|
|
|
|
if (gethostname(hostname, sizeof(hostname)) == -1)
|
|
fatal("gethostname");
|
|
|
|
if ((pw = getpwuid(getuid())) == NULL)
|
|
fatal("getpwuid");
|
|
|
|
memset(¶ms, 0, sizeof(params));
|
|
|
|
params.linemax = 16392;
|
|
params.ibufmax = 65536;
|
|
params.obufmax = 65536;
|
|
params.timeout = 100000;
|
|
params.helo = hostname;
|
|
|
|
params.tls_verify = 1;
|
|
|
|
memset(&mail, 0, sizeof(mail));
|
|
mail.from = pw->pw_name;
|
|
|
|
while ((ch = getopt(argc, argv, "CF:H:S:T:a:hns:v")) != -1) {
|
|
switch (ch) {
|
|
case 'C':
|
|
params.tls_verify = 0;
|
|
break;
|
|
case 'F':
|
|
mail.from = optarg;
|
|
break;
|
|
case 'H':
|
|
params.helo = optarg;
|
|
break;
|
|
case 'S':
|
|
servname = optarg;
|
|
break;
|
|
case 'T':
|
|
parse_tls_options(optarg);
|
|
break;
|
|
case 'a':
|
|
if ((authfile = fopen(optarg, "r")) == NULL)
|
|
fatal("%s: open", optarg);
|
|
if ((len = getline(&authstr, &alloc, authfile)) == -1)
|
|
fatal("%s: Failed to read username", optarg);
|
|
if (authstr[len - 1] == '\n')
|
|
authstr[len - 1] = '\0';
|
|
params.auth_user = authstr;
|
|
authstr = NULL;
|
|
len = 0;
|
|
if ((len = getline(&authstr, &alloc, authfile)) == -1)
|
|
fatal("%s: Failed to read password", optarg);
|
|
if (authstr[len - 1] == '\n')
|
|
authstr[len - 1] = '\0';
|
|
params.auth_pass = authstr;
|
|
fclose(authfile);
|
|
break;
|
|
case 'h':
|
|
usage();
|
|
break;
|
|
case 'n':
|
|
noaction = 1;
|
|
break;
|
|
case 's':
|
|
server = optarg;
|
|
break;
|
|
case 'v':
|
|
verbose++;
|
|
break;
|
|
default:
|
|
usage();
|
|
}
|
|
}
|
|
|
|
log_setverbose(verbose);
|
|
|
|
argc -= optind;
|
|
argv += optind;
|
|
|
|
if (argc) {
|
|
mail.rcpt = calloc(argc, sizeof(*mail.rcpt));
|
|
if (mail.rcpt == NULL)
|
|
fatal("calloc");
|
|
for (i = 0; i < argc; i++)
|
|
mail.rcpt[i].to = argv[i];
|
|
mail.rcptcount = argc;
|
|
}
|
|
|
|
event_init();
|
|
|
|
tls_config = tls_config_new();
|
|
if (tls_config == NULL)
|
|
fatal("tls_config_new");
|
|
|
|
if (protocols) {
|
|
if (tls_config_parse_protocols(&protos, protocols) == -1)
|
|
fatalx("failed to parse protocol '%s'", protocols);
|
|
if (tls_config_set_protocols(tls_config, protos) == -1)
|
|
fatalx("tls_config_set_protocols: %s",
|
|
tls_config_error(tls_config));
|
|
}
|
|
if (ciphers && tls_config_set_ciphers(tls_config, ciphers) == -1)
|
|
fatalx("tls_config_set_ciphers: %s",
|
|
tls_config_error(tls_config));
|
|
|
|
if (cafile == NULL)
|
|
cafile = tls_default_ca_cert_file();
|
|
if (tls_config_set_ca_file(tls_config, cafile) == -1)
|
|
fatal("tls_set_ca_file");
|
|
if (!params.tls_verify) {
|
|
tls_config_insecure_noverifycert(tls_config);
|
|
tls_config_insecure_noverifyname(tls_config);
|
|
tls_config_insecure_noverifytime(tls_config);
|
|
} else
|
|
tls_config_verify(tls_config);
|
|
|
|
if (pledge("stdio inet dns tmppath", NULL) == -1)
|
|
fatal("pledge");
|
|
|
|
if (!noaction)
|
|
parse_message(stdin);
|
|
|
|
if (pledge("stdio inet dns", NULL) == -1)
|
|
fatal("pledge");
|
|
|
|
parse_server(server);
|
|
|
|
if (pledge("stdio inet", NULL) == -1)
|
|
fatal("pledge");
|
|
|
|
resume();
|
|
|
|
log_debug("done...");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
parse_server(char *server)
|
|
{
|
|
struct addrinfo hints;
|
|
char *scheme, *creds, *host, *port, *p, *c;
|
|
int error;
|
|
|
|
creds = NULL;
|
|
host = NULL;
|
|
port = NULL;
|
|
scheme = server;
|
|
|
|
p = strstr(server, "://");
|
|
if (p) {
|
|
*p = '\0';
|
|
p += 3;
|
|
/* check for credentials */
|
|
c = strrchr(p, '@');
|
|
if (c) {
|
|
creds = p;
|
|
*c = '\0';
|
|
host = c + 1;
|
|
} else
|
|
host = p;
|
|
} else {
|
|
/* Assume a simple server name */
|
|
scheme = "smtp";
|
|
host = server;
|
|
}
|
|
|
|
if (host[0] == '[') {
|
|
/* IPV6 address? */
|
|
p = strchr(host, ']');
|
|
if (p) {
|
|
if (p[1] == ':' || p[1] == '\0') {
|
|
*p++ = '\0'; /* remove ']' */
|
|
host++; /* skip '[' */
|
|
if (*p == ':')
|
|
port = p + 1;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
port = strchr(host, ':');
|
|
if (port)
|
|
*port++ = '\0';
|
|
}
|
|
|
|
if (port && port[0] == '\0')
|
|
port = NULL;
|
|
|
|
if (creds) {
|
|
p = strchr(creds, ':');
|
|
if (p == NULL)
|
|
fatalx("invalid credentials");
|
|
*p = '\0';
|
|
|
|
params.auth_user = creds;
|
|
params.auth_pass = p + 1;
|
|
}
|
|
params.tls_req = TLS_YES;
|
|
|
|
if (!strcmp(scheme, "lmtp")) {
|
|
params.lmtp = 1;
|
|
}
|
|
else if (!strcmp(scheme, "lmtp+tls")) {
|
|
params.lmtp = 1;
|
|
params.tls_req = TLS_FORCE;
|
|
}
|
|
else if (!strcmp(scheme, "lmtp+notls")) {
|
|
params.lmtp = 1;
|
|
params.tls_req = TLS_NO;
|
|
}
|
|
else if (!strcmp(scheme, "smtps")) {
|
|
params.tls_req = TLS_SMTPS;
|
|
if (port == NULL)
|
|
port = "smtps";
|
|
}
|
|
else if (!strcmp(scheme, "smtp")) {
|
|
}
|
|
else if (!strcmp(scheme, "smtp+tls")) {
|
|
params.tls_req = TLS_FORCE;
|
|
}
|
|
else if (!strcmp(scheme, "smtp+notls")) {
|
|
params.tls_req = TLS_NO;
|
|
}
|
|
else
|
|
fatalx("invalid url scheme %s", scheme);
|
|
|
|
if (port == NULL)
|
|
port = "smtp";
|
|
|
|
if (servname == NULL)
|
|
servname = host;
|
|
if (nosni == 0)
|
|
params.tls_servname = servname;
|
|
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
error = getaddrinfo(host, port, &hints, &res0);
|
|
if (error)
|
|
fatalx("%s: %s", host, gai_strerror(error));
|
|
ai = res0;
|
|
}
|
|
|
|
void
|
|
parse_message(FILE *ifp)
|
|
{
|
|
char *line = NULL;
|
|
size_t linesz = 0;
|
|
ssize_t len;
|
|
|
|
if ((mail.fp = tmpfile()) == NULL)
|
|
fatal("tmpfile");
|
|
|
|
for (;;) {
|
|
if ((len = getline(&line, &linesz, ifp)) == -1) {
|
|
if (feof(ifp))
|
|
break;
|
|
fatal("getline");
|
|
}
|
|
|
|
if (len >= 2 && line[len - 2] == '\r' && line[len - 1] == '\n')
|
|
line[--len - 1] = '\n';
|
|
|
|
if (fwrite(line, 1, len, mail.fp) != len)
|
|
fatal("fwrite");
|
|
|
|
if (line[len - 1] != '\n' && fputc('\n', mail.fp) == EOF)
|
|
fatal("fputc");
|
|
}
|
|
|
|
fclose(ifp);
|
|
rewind(mail.fp);
|
|
}
|
|
|
|
void
|
|
resume(void)
|
|
{
|
|
static int started = 0;
|
|
char host[256];
|
|
char serv[16];
|
|
|
|
if (done) {
|
|
event_loopexit(NULL);
|
|
return;
|
|
}
|
|
|
|
if (ai == NULL)
|
|
fatalx("no more host");
|
|
|
|
getnameinfo(ai->ai_addr, ai->ai_addr->sa_len,
|
|
host, sizeof(host), serv, sizeof(serv),
|
|
NI_NUMERICHOST | NI_NUMERICSERV);
|
|
log_debug("trying host %s port %s...", host, serv);
|
|
|
|
params.dst = ai->ai_addr;
|
|
if (smtp_connect(¶ms, NULL) == NULL)
|
|
fatal("smtp_connect");
|
|
|
|
if (started == 0) {
|
|
started = 1;
|
|
event_loop(0);
|
|
}
|
|
}
|
|
|
|
void
|
|
log_trace(int lvl, const char *emsg, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
if (verbose > lvl) {
|
|
va_start(ap, emsg);
|
|
vlog(LOG_DEBUG, emsg, ap);
|
|
va_end(ap);
|
|
}
|
|
}
|
|
|
|
void
|
|
smtp_require_tls(void *tag, struct smtp_client *proto)
|
|
{
|
|
struct tls *tls;
|
|
|
|
tls = tls_client();
|
|
if (tls == NULL)
|
|
fatal("tls_client");
|
|
|
|
if (tls_configure(tls, tls_config) == -1)
|
|
fatal("tls_configure");
|
|
|
|
smtp_set_tls(proto, tls);
|
|
}
|
|
|
|
void
|
|
smtp_ready(void *tag, struct smtp_client *proto)
|
|
{
|
|
log_debug("connection ready...");
|
|
|
|
if (done || noaction)
|
|
smtp_quit(proto);
|
|
else
|
|
smtp_sendmail(proto, &mail);
|
|
}
|
|
|
|
void
|
|
smtp_failed(void *tag, struct smtp_client *proto, int failure, const char *detail)
|
|
{
|
|
switch (failure) {
|
|
case FAIL_INTERNAL:
|
|
log_warnx("internal error: %s", detail);
|
|
break;
|
|
case FAIL_CONN:
|
|
log_warnx("connection error: %s", detail);
|
|
break;
|
|
case FAIL_PROTO:
|
|
log_warnx("protocol error: %s", detail);
|
|
break;
|
|
case FAIL_IMPL:
|
|
log_warnx("missing feature: %s", detail);
|
|
break;
|
|
case FAIL_RESP:
|
|
log_warnx("rejected by server: %s", detail);
|
|
break;
|
|
default:
|
|
fatalx("unknown failure %d: %s", failure, detail);
|
|
}
|
|
}
|
|
|
|
void
|
|
smtp_status(void *tag, struct smtp_client *proto, struct smtp_status *status)
|
|
{
|
|
log_info("%s: %s: %s", status->rcpt->to, status->cmd, status->status);
|
|
}
|
|
|
|
void
|
|
smtp_done(void *tag, struct smtp_client *proto, struct smtp_mail *mail)
|
|
{
|
|
int i;
|
|
|
|
log_debug("mail done...");
|
|
|
|
if (noaction)
|
|
return;
|
|
|
|
for (i = 0; i < mail->rcptcount; i++)
|
|
if (!mail->rcpt[i].done)
|
|
return;
|
|
|
|
done = 1;
|
|
}
|
|
|
|
void
|
|
smtp_closed(void *tag, struct smtp_client *proto)
|
|
{
|
|
log_debug("connection closed...");
|
|
|
|
ai = ai->ai_next;
|
|
if (noaction && ai == NULL)
|
|
done = 1;
|
|
|
|
resume();
|
|
}
|