*
* 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;
char *opt_from;
char *s;
llist_t *list = NULL;
- char *domain = sane_address(safe_getdomainname());
+ 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 {
//--- standard options
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
- // -f is required. -H and -S are mutually exclusive
- opt_complementary = "f:w+:H--S:S--H:a::";
+ // -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::", &opt_from, NULL, &timeout, &opt_connect, &opt_connect, &list);
+ opts = getopt32(argv, "tf:o:iw:H:S:a::v", &opt_from, NULL,
+ &timeout, &opt_connect, &opt_connect, &list, &verbose);
//argc -= optind;
argv += optind;
const char *args[] = { "sh", "-c", opt_connect, NULL };
// plug it in
launch_helper(args);
- // vanilla connection
+ // 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
+ // 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 kick 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 (ENABLE_FEATURE_CLEAN_UP)
- free(domain);
+ if (250 != smtp_checkp("EHLO %s", host, -1))
+ smtp_checkp("HELO %s", host, 250);
+ free(host);
// perform authentication
if (opts & OPT_a) {
}
// 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.
// G.user = xuid2uname(getuid());
// opt_from = xasprintf("%s@%s", G.user, domain);
//}
- //if (ENABLE_FEATURE_CLEAN_UP)
- // free(domain);
smtp_checkp("MAIL FROM:<%s>", opt_from, 250);
// process message
// 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
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
- if (0 == strncasecmp("To: ", s, 4) || 0 == strncasecmp("Bcc: " + 1, s, 4)) {
- rcptto(sane_address(s+4));
-// goto addh;
- llist_add_to_end(&list, s);
- // Bcc: header adds blind copy (hidden) recipient
- } else if (0 == strncasecmp("Bcc: ", s, 5)) {
- rcptto(sane_address(s+5));
- free(s);
- // N.B. Bcc: vanishes from headers!
- // other headers go verbatim
- } else if (s[0]) {
-// addh:
+ 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);
- // the empty line stops analyzing headers
} else {
- free(s);
+ // 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) {
- s = sane_address(*argv);
- rcptto(s);
- llist_add_to_end(&list, xasprintf("To: %s", s));
+ 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
- smtp_check("DATA", 354);
+ // 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) {
- printf("%s\r\n", (char *) llist_pop(&list));
+ send_r_n((char *) llist_pop(&list));
}
- printf("%s\r\n" + 2); // quirk for format string to be reused
// 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;
// finalize the message
smtp_check(".", 250);
+ bail:
// ... and say goodbye
smtp_check("QUIT", 221);
// cleanup