libbb: move nuke_str() from passwd into libbb
[oweals/busybox.git] / mailutils / sendmail.c
index 55555c326fdc79bb8e98f26579b5babf2892a677..b5aa1d17bc1d159b8500f9119396dad626cbcc60 100644 (file)
@@ -4,31 +4,82 @@
  *
  * 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);
 }
@@ -41,55 +92,103 @@ static int smtp_check(const char *fmt, int code)
 // 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
@@ -97,39 +196,71 @@ int sendmail_main(int argc UNUSED_PARAM, char **argv)
 
        // 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)
@@ -140,26 +271,30 @@ int sendmail_main(int argc UNUSED_PARAM, char **argv)
                // 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.
@@ -170,201 +305,24 @@ int sendmail_main(int argc UNUSED_PARAM, char **argv)
        //      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
@@ -372,12 +330,106 @@ int sendmail_main(int argc UNUSED_PARAM, char **argv)
                        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