*
* Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
*
- * Licensed under GPLv2, see file LICENSE in this tarball for details.
+ * Licensed under GPLv2, see file LICENSE in this source tree.
*/
+
+//kbuild:lib-$(CONFIG_SENDMAIL) += sendmail.o mail.o
+
+//usage:#define sendmail_trivial_usage
+//usage: "[OPTIONS] [RECIPIENT_EMAIL]..."
+//usage:#define sendmail_full_usage "\n\n"
+//usage: "Read email from stdin and send it\n"
+//usage: "\nStandard options:"
+//usage: "\n -t Read additional recipients from message body"
+//usage: "\n -f SENDER Sender (required)"
+//usage: "\n -o OPTIONS Various options. -oi implied, others are ignored"
+//usage: "\n -i -oi synonym. implied and ignored"
+//usage: "\n"
+//usage: "\nBusybox specific options:"
+//usage: "\n -v Verbose"
+//usage: "\n -w SECS Network timeout"
+//usage: "\n -H 'PROG ARGS' Run connection helper"
+//usage: "\n Examples:"
+//usage: "\n -H 'exec openssl s_client -quiet -tls1 -starttls smtp"
+//usage: "\n -connect smtp.gmail.com:25' <email.txt"
+//usage: "\n [4<username_and_passwd.txt | -auUSER -apPASS]"
+//usage: "\n -H 'exec openssl s_client -quiet -tls1"
+//usage: "\n -connect smtp.gmail.com:465' <email.txt"
+//usage: "\n [4<username_and_passwd.txt | -auUSER -apPASS]"
+//usage: "\n -S HOST[:PORT] Server"
+//usage: "\n -auUSER Username for AUTH LOGIN"
+//usage: "\n -apPASS Password for AUTH LOGIN"
+////usage: "\n -amMETHOD Authentication method. Ignored. LOGIN is implied"
+//usage: "\n"
+//usage: "\nOther options are silently ignored; -oi -t is implied"
+//usage: IF_MAKEMIME(
+//usage: "\nUse makemime to create emails with attachments"
+//usage: )
+
#include "libbb.h"
#include "mail.h"
+// limit maximum allowed number of headers to prevent overflows.
+// set to 0 to not limit
+#define MAX_HEADERS 256
+
+static void send_r_n(const char *s)
+{
+ if (verbose)
+ bb_error_msg("send:'%s'", s);
+ printf("%s\r\n", s);
+}
+
static int smtp_checkp(const char *fmt, const char *param, int code)
{
char *answer;
- const char *msg = command(fmt, param);
+ char *msg = send_mail_command(fmt, param);
// read stdin
- // if the string has a form \d\d\d- -- read next string. E.g. EHLO response
+ // if the string has a form NNN- -- read next string. E.g. EHLO response
// parse first bytes to a number
// if code = -1 then just return this number
// if code != -1 then checks whether the number equals the code
// if not equal -> die saying msg
- while ((answer = xmalloc_fgetline(stdin)) != NULL)
+ while ((answer = xmalloc_fgetline(stdin)) != NULL) {
+ if (verbose)
+ bb_error_msg("recv:'%.*s'", (int)(strchrnul(answer, '\r') - answer), answer);
if (strlen(answer) <= 3 || '-' != answer[3])
break;
+ free(answer);
+ }
if (answer) {
int n = atoi(answer);
if (timeout)
alarm(0);
free(answer);
- if (-1 == code || n == code)
+ if (-1 == code || n == code) {
+ free(msg);
return n;
+ }
}
bb_error_msg_and_die("%s failed", msg);
}
// strip argument of bad chars
static char *sane_address(char *str)
{
- char *s = str;
- char *p = s;
+ char *s;
+
+ trim(str);
+ s = str;
while (*s) {
- if (isalnum(*s) || '_' == *s || '-' == *s || '.' == *s || '@' == *s) {
- *p++ = *s;
+ if (!isalnum(*s) && !strchr("_-.@", *s)) {
+ bb_error_msg("bad address '%s'", str);
+ /* returning "": */
+ str[0] = '\0';
+ return str;
}
s++;
}
- *p = '\0';
return str;
}
+// check for an address inside angle brackets, if not found fall back to normal
+static char *angle_address(char *str)
+{
+ char *s, *e;
+
+ trim(str);
+ e = last_char_is(str, '>');
+ if (e) {
+ s = strrchr(str, '<');
+ if (s) {
+ *e = '\0';
+ str = s + 1;
+ }
+ }
+ return sane_address(str);
+}
+
static void rcptto(const char *s)
{
- smtp_checkp("RCPT TO:<%s>", s, 250);
+ if (!*s)
+ return;
+ // N.B. we don't die if recipient is rejected, for the other recipients may be accepted
+ if (250 != smtp_checkp("RCPT TO:<%s>", s, -1))
+ bb_error_msg("Bad recipient: <%s>", s);
+}
+
+// send to a list of comma separated addresses
+static void rcptto_list(const char *list)
+{
+ char *str = xstrdup(list);
+ char *s = str;
+ char prev = 0;
+ int in_quote = 0;
+
+ while (*s) {
+ char ch = *s++;
+
+ if (ch == '"' && prev != '\\') {
+ in_quote = !in_quote;
+ } else if (!in_quote && ch == ',') {
+ s[-1] = '\0';
+ rcptto(angle_address(str));
+ str = s;
+ }
+ prev = ch;
+ }
+ if (prev != ',')
+ rcptto(angle_address(str));
+ free(str);
}
int sendmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int sendmail_main(int argc UNUSED_PARAM, char **argv)
{
-#if ENABLE_FEATURE_SENDMAIL_MAILX
- llist_t *opt_attachments = NULL;
- const char *opt_subject;
-#if ENABLE_FEATURE_SENDMAIL_MAILXX
- llist_t *opt_carboncopies = NULL;
- char *opt_errors_to;
-#endif
-#endif
char *opt_connect = opt_connect;
- char *opt_from, *opt_fullname;
- char *boundary;
- llist_t *l;
- llist_t *headers = NULL;
- char *domain = sane_address(safe_getdomainname());
+ char *opt_from;
+ char *s;
+ llist_t *list = NULL;
+ char *host = sane_address(safe_gethostname());
+ unsigned nheaders = 0;
int code;
+ enum {
+ HDR_OTHER = 0,
+ HDR_TOCC,
+ HDR_BCC,
+ } last_hdr = 0;
+ int check_hdr;
+ int has_to = 0;
enum {
- OPT_w = 1 << 0, // network timeout
- OPT_t = 1 << 1, // read message for recipients
- OPT_N = 1 << 2, // request notification
- OPT_f = 1 << 3, // sender address
- OPT_F = 1 << 4, // sender name, overrides $NAME
- OPT_s = 1 << 5, // subject
- OPT_j = 1 << 6, // assumed charset
- OPT_a = 1 << 7, // attachment(s)
- OPT_H = 1 << 8, // use external connection helper
- OPT_S = 1 << 9, // specify connection string
- OPT_c = 1 << 10, // carbon copy
- OPT_e = 1 << 11, // errors-to address
+ //--- standard options
+ OPT_t = 1 << 0, // read message for recipients, append them to those on cmdline
+ OPT_f = 1 << 1, // sender address
+ OPT_o = 1 << 2, // various options. -oi IMPLIED! others are IGNORED!
+ OPT_i = 1 << 3, // IMPLIED!
+ //--- BB specific options
+ OPT_w = 1 << 4, // network timeout
+ OPT_H = 1 << 5, // use external connection helper
+ OPT_S = 1 << 6, // specify connection string
+ OPT_a = 1 << 7, // authentication tokens
+ OPT_v = 1 << 8, // verbosity
};
// init global variables
// save initial stdin since body is piped!
xdup2(STDIN_FILENO, 3);
- G.fp0 = fdopen(3, "r");
+ G.fp0 = xfdopen_for_read(3);
// parse options
- opt_complementary = "w+" USE_FEATURE_SENDMAIL_MAILX(":a::H--S:S--H") USE_FEATURE_SENDMAIL_MAILXX(":c::");
- opts = getopt32(argv,
- "w:t" "N:f:F:" USE_FEATURE_SENDMAIL_MAILX("s:j:a:H:S:") USE_FEATURE_SENDMAIL_MAILXX("c:e:")
- "X:V:vq:R:O:o:nmL:Iih:GC:B:b:A:" // postfix compat only, ignored
- // r:Q:p:M:Dd are candidates from another man page. TODO?
- "46E", // ssmtp introduces another quirks. TODO?: -a[upm] (user, pass, method) to be supported
- &timeout /* -w */, NULL, &opt_from, &opt_fullname,
- USE_FEATURE_SENDMAIL_MAILX(&opt_subject, &G.opt_charset, &opt_attachments, &opt_connect, &opt_connect,)
- USE_FEATURE_SENDMAIL_MAILXX(&opt_carboncopies, &opt_errors_to,)
- NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
- );
+ // -v is a counter, -f is required. -H and -S are mutually exclusive, -a is a list
+ opt_complementary = "vv:f:w+:H--S:S--H:a::";
+ // N.B. since -H and -S are mutually exclusive they do not interfere in opt_connect
+ // -a is for ssmtp (http://downloads.openwrt.org/people/nico/man/man8/ssmtp.8.html) compatibility,
+ // it is still under development.
+ opts = getopt32(argv, "tf:o:iw:H:S:a::v", &opt_from, NULL,
+ &timeout, &opt_connect, &opt_connect, &list, &verbose);
//argc -= optind;
argv += optind;
+ // process -a[upm]<token> options
+ if ((opts & OPT_a) && !list)
+ bb_show_usage();
+ while (list) {
+ char *a = (char *) llist_pop(&list);
+ if ('u' == a[0])
+ G.user = xstrdup(a+1);
+ if ('p' == a[0])
+ G.pass = xstrdup(a+1);
+ // N.B. we support only AUTH LOGIN so far
+ //if ('m' == a[0])
+ // G.method = xstrdup(a+1);
+ }
+ // N.B. list == NULL here
+ //bb_info_msg("OPT[%x] AU[%s], AP[%s], AM[%s], ARGV[%s]", opts, au, ap, am, *argv);
+
// connect to server
-#if ENABLE_FEATURE_SENDMAIL_MAILX
- // N.B. -H and -S are mutually exclusive so they do not spoil opt_connect
// connection helper ordered? ->
if (opts & OPT_H) {
const char *args[] = { "sh", "-c", opt_connect, NULL };
// plug it in
launch_helper(args);
- // vanilla connection
- } else
-#endif
- {
+ // Now:
+ // our stdout will go to helper's stdin,
+ // helper's stdout will be available on our stdin.
+
+ // Wait for initial server message.
+ // If helper (such as openssl) invokes STARTTLS, the initial 220
+ // is swallowed by helper (and not repeated after TLS is initiated).
+ // We will send NOOP cmd to server and check the response.
+ // We should get 220+250 on plain connection, 250 on STARTTLSed session.
+ //
+ // The problem here is some servers delay initial 220 message,
+ // and consider client to be a spammer if it starts sending cmds
+ // before 220 reached it. The code below is unsafe in this regard:
+ // in non-STARTTLSed case, we potentially send NOOP before 220
+ // is sent by server.
+ // Ideas? (--delay SECS opt? --assume-starttls-helper opt?)
+ code = smtp_check("NOOP", -1);
+ if (code == 220)
+ // we got 220 - this is not STARTTLSed connection,
+ // eat 250 response to our NOOP
+ smtp_check(NULL, 250);
+ else
+ if (code != 250)
+ bb_error_msg_and_die("SMTP init failed");
+ } else {
+ // vanilla connection
int fd;
- // host[:port] not explicitly specified ? -> use $SMTPHOST
- // no $SMTPHOST ? -> use localhost
+ // host[:port] not explicitly specified? -> use $SMTPHOST
+ // no $SMTPHOST? -> use localhost
if (!(opts & OPT_S)) {
opt_connect = getenv("SMTPHOST");
if (!opt_connect)
// and make ourselves a simple IO filter
xmove_fd(fd, STDIN_FILENO);
xdup2(STDIN_FILENO, STDOUT_FILENO);
+
+ // Wait for initial server 220 message
+ smtp_check(NULL, 220);
}
- // N.B. from now we know nothing about network :)
-
- // wait for initial server OK
- // N.B. if we used openssl the initial 220 answer is already swallowed during openssl TLS init procedure
- // so we need to push the server to see whether we are ok
- code = smtp_check("NOOP", -1);
- // 220 on plain connection, 250 on openssl-helped TLS session
- if (220 == code)
- smtp_check(NULL, 250); // reread the code to stay in sync
- else if (250 != code)
- bb_error_msg_and_die("INIT failed");
// we should start with modern EHLO
- if (250 != smtp_checkp("EHLO %s", domain, -1)) {
- smtp_checkp("HELO %s", domain, 250);
+ if (250 != smtp_checkp("EHLO %s", host, -1))
+ smtp_checkp("HELO %s", host, 250);
+ free(host);
+
+ // perform authentication
+ if (opts & OPT_a) {
+ smtp_check("AUTH LOGIN", 334);
+ // we must read credentials unless they are given via -a[up] options
+ if (!G.user || !G.pass)
+ get_cred_or_die(4);
+ encode_base64(NULL, G.user, NULL);
+ smtp_check("", 334);
+ encode_base64(NULL, G.pass, NULL);
+ smtp_check("", 235);
}
// set sender
- // N.B. we have here a very loosely defined algotythm
+ // N.B. we have here a very loosely defined algorythm
// since sendmail historically offers no means to specify secrets on cmdline.
// 1) server can require no authentication ->
// we must just provide a (possibly fake) reply address.
// file descriptor (e.g. 4), or again from a secured file.
// got no sender address? -> use system username as a resort
- if (!(opts & OPT_f)) {
- // N.B. IMHO getenv("USER") can be way easily spoofed!
- G.user = bb_getpwuid(NULL, -1, getuid());
- opt_from = xasprintf("%s@%s", G.user, domain);
- }
- if (ENABLE_FEATURE_CLEAN_UP)
- free(domain);
-
- code = -1; // first try softly without authentication
- while (250 != smtp_checkp("MAIL FROM:<%s>", opt_from, code)) {
- // MAIL FROM failed -> authentication needed
- if (334 == smtp_check("AUTH LOGIN", -1)) {
- // we must read credentials
- get_cred_or_die(4);
- encode_base64(NULL, G.user, NULL);
- smtp_check("", 334);
- encode_base64(NULL, G.pass, NULL);
- smtp_check("", 235);
- }
- // authenticated OK? -> retry to set sender
- // but this time die on failure!
- code = 250;
- }
-
- // recipients specified as arguments
- while (*argv) {
- char *s = sane_address(*argv);
- // loose test on email address validity
-// if (strchr(s, '@')) {
- rcptto(s);
- llist_add_to_end(&headers, xasprintf("To: %s", s));
-// }
- argv++;
- }
-
-#if ENABLE_FEATURE_SENDMAIL_MAILXX
- // carbon copies recipients specified as -c options
- for (l = opt_carboncopies; l; l = l->link) {
- char *s = sane_address(l->data);
- // loose test on email address validity
-// if (strchr(s, '@')) {
- rcptto(s);
- // TODO: do we ever need to mangle the message?
- //llist_add_to_end(&headers, xasprintf("Cc: %s", s));
-// }
- }
-#endif
-
- // if -t specified or no recipients specified -> read recipients from message
- // i.e. scan stdin for To:, Cc:, Bcc: lines ...
- // ... and then use the rest of stdin as message body
- // N.B. subject read from body can be further overrided with one specified on command line.
- // recipients are merged. Bcc: lines are deleted
- // N.B. other headers are collected and will be dumped verbatim
- if (opts & OPT_t || !headers) {
- // fetch recipients and (optionally) subject
- char *s;
- while ((s = xmalloc_fgetline(G.fp0)) != NULL) {
- if (0 == strncasecmp("To: ", s, 4) || 0 == strncasecmp("Cc: ", s, 4)) {
- rcptto(sane_address(s+4));
- llist_add_to_end(&headers, s);
- } else if (0 == strncasecmp("Bcc: ", s, 5)) {
- rcptto(sane_address(s+5));
- free(s);
- // N.B. Bcc vanishes from headers!
- } else if (0 == strncmp("Subject: ", s, 9)) {
- // we read subject -> use it verbatim unless it is specified
- // on command line
- if (!(opts & OPT_s))
- llist_add_to_end(&headers, s);
- else
- free(s);
- } else if (s[0]) {
- // misc header
- llist_add_to_end(&headers, s);
- } else {
- free(s);
- break; // stop on the first empty line
- }
- }
- }
-
- // enter "put message" mode
- smtp_check("DATA", 354);
-
- // put headers we could have preread with -t
- for (l = headers; l; l = l->link) {
- printf("%s\r\n", l->data);
- if (ENABLE_FEATURE_CLEAN_UP)
- free(l->data);
- }
-
- // put (possibly encoded) subject
-#if ENABLE_FEATURE_SENDMAIL_MAILX
- if (opts & OPT_s) {
- printf("Subject: ");
- if (opts & OPT_j) {
- printf("=?%s?B?", G.opt_charset);
- encode_base64(NULL, opt_subject, NULL);
- printf("?=");
- } else {
- printf("%s", opt_subject);
- }
- printf("\r\n");
- }
-#endif
-
- // put sender name, $NAME is the default
- if (!(opts & OPT_F))
- opt_fullname = getenv("NAME");
- if (opt_fullname)
- printf("From: \"%s\" <%s>\r\n", opt_fullname, opt_from);
-
- // put notification
- if (opts & OPT_N)
- printf("Disposition-Notification-To: %s\r\n", opt_from);
-
-#if ENABLE_FEATURE_SENDMAIL_MAILXX
- // put errors recipient
- if (opts & OPT_e)
- printf("Errors-To: %s\r\n", opt_errors_to);
-#endif
-
- // make a random string -- it will delimit message parts
- srand(monotonic_us());
- boundary = xasprintf("%d=_%d-%d", rand(), rand(), rand());
-
- // put common headers
- // TODO: do we really need this?
-// printf("Message-ID: <%s>\r\n", boundary);
-
-#if ENABLE_FEATURE_SENDMAIL_MAILX
- // have attachments? -> compose multipart MIME
- if (opt_attachments) {
- const char *fmt;
- const char *p;
- char *q;
-
- printf(
- "Mime-Version: 1.0\r\n"
- "%smultipart/mixed; boundary=\"%s\"\r\n"
- , "Content-Type: "
- , boundary
- );
-
- // body is pseudo attachment read from stdin in first turn
- llist_add_to(&opt_attachments, (char *)"-");
-
- // put body + attachment(s)
- // N.B. all these weird things just to be tiny
- // by reusing string patterns!
- fmt =
- "\r\n--%s\r\n"
- "%stext/plain; charset=%s\r\n"
- "%s%s\r\n"
- "%s"
- ;
- p = G.opt_charset;
- q = (char *)"";
- l = opt_attachments;
- while (l) {
- printf(
- fmt
- , boundary
- , "Content-Type: "
- , p
- , "Content-Disposition: inline"
- , q
- , "Content-Transfer-Encoding: base64\r\n"
- );
- p = "";
- fmt =
- "\r\n--%s\r\n"
- "%sapplication/octet-stream%s\r\n"
- "%s; filename=\"%s\"\r\n"
- "%s"
- ;
- encode_base64(l->data, (const char *)G.fp0, "\r");
- l = l->link;
- if (l)
- q = bb_get_last_path_component_strip(l->data);
- }
-
- // put message terminator
- printf("\r\n--%s--\r\n" "\r\n", boundary);
-
- // no attachments? -> just dump message
- } else
-#endif
- {
- char *s;
- // terminate headers
- printf("\r\n");
- // put plain text respecting leading dots
- while ((s = xmalloc_fgetline(G.fp0)) != NULL) {
+ // N.B. we marked -f as required option!
+ //if (!G.user) {
+ // // N.B. IMHO getenv("USER") can be way easily spoofed!
+ // G.user = xuid2uname(getuid());
+ // opt_from = xasprintf("%s@%s", G.user, domain);
+ //}
+ smtp_checkp("MAIL FROM:<%s>", opt_from, 250);
+
+ // process message
+
+ // read recipients from message and add them to those given on cmdline.
+ // this means we scan stdin for To:, Cc:, Bcc: lines until an empty line
+ // and then use the rest of stdin as message body
+ code = 0; // set "analyze headers" mode
+ while ((s = xmalloc_fgetline(G.fp0)) != NULL) {
+ dump:
+ // put message lines doubling leading dots
+ if (code) {
// escape leading dots
// N.B. this feature is implied even if no -i (-oi) switch given
// N.B. we need to escape the leading dot regardless of
if ('.' == s[0] /*&& '\0' == s[1] */)
printf(".");
// dump read line
- printf("%s\r\n", s);
+ send_r_n(s);
+ free(s);
+ continue;
+ }
+
+ // analyze headers
+ // To: or Cc: headers add recipients
+ check_hdr = (0 == strncasecmp("To:", s, 3));
+ has_to |= check_hdr;
+ if (opts & OPT_t) {
+ if (check_hdr || 0 == strncasecmp("Bcc:" + 1, s, 3)) {
+ rcptto_list(s+3);
+ last_hdr = HDR_TOCC;
+ goto addheader;
+ }
+ // Bcc: header adds blind copy (hidden) recipient
+ if (0 == strncasecmp("Bcc:", s, 4)) {
+ rcptto_list(s+4);
+ free(s);
+ last_hdr = HDR_BCC;
+ continue; // N.B. Bcc: vanishes from headers!
+ }
+ }
+ check_hdr = (list && isspace(s[0]));
+ if (strchr(s, ':') || check_hdr) {
+ // other headers go verbatim
+ // N.B. RFC2822 2.2.3 "Long Header Fields" allows for headers to occupy several lines.
+ // Continuation is denoted by prefixing additional lines with whitespace(s).
+ // Thanks (stefan.seyfried at googlemail.com) for pointing this out.
+ if (check_hdr && last_hdr != HDR_OTHER) {
+ rcptto_list(s+1);
+ if (last_hdr == HDR_BCC)
+ continue;
+ // N.B. Bcc: vanishes from headers!
+ } else {
+ last_hdr = HDR_OTHER;
+ }
+ addheader:
+ // N.B. we allow MAX_HEADERS generic headers at most to prevent attacks
+ if (MAX_HEADERS && ++nheaders >= MAX_HEADERS)
+ goto bail;
+ llist_add_to_end(&list, s);
+ } else {
+ // a line without ":" (an empty line too, by definition) doesn't look like a valid header
+ // so stop "analyze headers" mode
+ reenter:
+ // put recipients specified on cmdline
+ check_hdr = 1;
+ while (*argv) {
+ char *t = sane_address(*argv);
+ rcptto(t);
+ //if (MAX_HEADERS && ++nheaders >= MAX_HEADERS)
+ // goto bail;
+ if (!has_to) {
+ const char *hdr;
+
+ if (check_hdr && argv[1])
+ hdr = "To: %s,";
+ else if (check_hdr)
+ hdr = "To: %s";
+ else if (argv[1])
+ hdr = "To: %s," + 3;
+ else
+ hdr = "To: %s" + 3;
+ llist_add_to_end(&list,
+ xasprintf(hdr, t));
+ check_hdr = 0;
+ }
+ argv++;
+ }
+ // enter "put message" mode
+ // N.B. DATA fails iff no recipients were accepted (or even provided)
+ // in this case just bail out gracefully
+ if (354 != smtp_check("DATA", -1))
+ goto bail;
+ // dump the headers
+ while (list) {
+ send_r_n((char *) llist_pop(&list));
+ }
+ // stop analyzing headers
+ code++;
+ // N.B. !s means: we read nothing, and nothing to be read in the future.
+ // just dump empty line and break the loop
+ if (!s) {
+ send_r_n("");
+ break;
+ }
+ // go dump message body
+ // N.B. "s" already contains the first non-header line, so pretend we read it from input
+ goto dump;
}
}
+ // odd case: we didn't stop "analyze headers" mode -> message body is empty. Reenter the loop
+ // N.B. after reenter code will be > 0
+ if (!code)
+ goto reenter;
- // leave "put message" mode
+ // finalize the message
smtp_check(".", 250);
+ bail:
// ... and say goodbye
smtp_check("QUIT", 221);
// cleanup