busybox/mailutils/sendmail.c
<<
>>
Prefs
   1/* vi: set sw=4 ts=4: */
   2/*
   3 * bare bones sendmail
   4 *
   5 * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com>
   6 *
   7 * Licensed under GPLv2, see file LICENSE in this tarball for details.
   8 */
   9#include "libbb.h"
  10#include "mail.h"
  11
  12static int smtp_checkp(const char *fmt, const char *param, int code)
  13{
  14        char *answer;
  15        const char *msg = command(fmt, param);
  16        // read stdin
  17        // if the string has a form \d\d\d- -- read next string. E.g. EHLO response
  18        // parse first bytes to a number
  19        // if code = -1 then just return this number
  20        // if code != -1 then checks whether the number equals the code
  21        // if not equal -> die saying msg
  22        while ((answer = xmalloc_fgetline(stdin)) != NULL)
  23                if (strlen(answer) <= 3 || '-' != answer[3])
  24                        break;
  25        if (answer) {
  26                int n = atoi(answer);
  27                if (timeout)
  28                        alarm(0);
  29                free(answer);
  30                if (-1 == code || n == code)
  31                        return n;
  32        }
  33        bb_error_msg_and_die("%s failed", msg);
  34}
  35
  36static int smtp_check(const char *fmt, int code)
  37{
  38        return smtp_checkp(fmt, NULL, code);
  39}
  40
  41// strip argument of bad chars
  42static char *sane_address(char *str)
  43{
  44        char *s = str;
  45        char *p = s;
  46        while (*s) {
  47                if (isalnum(*s) || '_' == *s || '-' == *s || '.' == *s || '@' == *s) {
  48                        *p++ = *s;
  49                }
  50                s++;
  51        }
  52        *p = '\0';
  53        return str;
  54}
  55
  56static void rcptto(const char *s)
  57{
  58        smtp_checkp("RCPT TO:<%s>", s, 250);
  59}
  60
  61int sendmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
  62int sendmail_main(int argc UNUSED_PARAM, char **argv)
  63{
  64        char *opt_connect = opt_connect;
  65        char *opt_from;
  66        char *s;
  67        llist_t *list = NULL;
  68        char *domain = sane_address(safe_getdomainname());
  69        int code;
  70
  71        enum {
  72        //--- standard options
  73                OPT_t = 1 << 0,         // read message for recipients, append them to those on cmdline
  74                OPT_f = 1 << 1,         // sender address
  75                OPT_o = 1 << 2,         // various options. -oi IMPLIED! others are IGNORED!
  76        //--- BB specific options
  77                OPT_w = 1 << 3,         // network timeout
  78                OPT_H = 1 << 4,         // use external connection helper
  79                OPT_S = 1 << 5,         // specify connection string
  80                OPT_a = 1 << 6,         // authentication tokens
  81        };
  82
  83        // init global variables
  84        INIT_G();
  85
  86        // save initial stdin since body is piped!
  87        xdup2(STDIN_FILENO, 3);
  88        G.fp0 = fdopen(3, "r");
  89
  90        // parse options
  91        // -f is required. -H and -S are mutually exclusive
  92        opt_complementary = "f:w+:H--S:S--H:a::";
  93        // N.B. since -H and -S are mutually exclusive they do not interfere in opt_connect
  94        // -a is for ssmtp (http://downloads.openwrt.org/people/nico/man/man8/ssmtp.8.html) compatibility,
  95        // it is still under development.
  96        opts = getopt32(argv, "tf:o:w:H:S:a::", &opt_from, NULL, &timeout, &opt_connect, &opt_connect, &list);
  97        //argc -= optind;
  98        argv += optind;
  99
 100        // process -a[upm]<token> options
 101        if ((opts & OPT_a) && !list)
 102                bb_show_usage();
 103        while (list) {
 104                char *a = (char *) llist_pop(&list);
 105                if ('u' == a[0])
 106                        G.user = xstrdup(a+1);
 107                if ('p' == a[0])
 108                        G.pass = xstrdup(a+1);
 109                // N.B. we support only AUTH LOGIN so far
 110                //if ('m' == a[0])
 111                //      G.method = xstrdup(a+1);
 112        }
 113        // N.B. list == NULL here
 114        //bb_info_msg("OPT[%x] AU[%s], AP[%s], AM[%s], ARGV[%s]", opts, au, ap, am, *argv);
 115
 116        // connect to server
 117
 118        // connection helper ordered? ->
 119        if (opts & OPT_H) {
 120                const char *args[] = { "sh", "-c", opt_connect, NULL };
 121                // plug it in
 122                launch_helper(args);
 123        // vanilla connection
 124        } else {
 125                int fd;
 126                // host[:port] not explicitly specified? -> use $SMTPHOST
 127                // no $SMTPHOST ? -> use localhost
 128                if (!(opts & OPT_S)) {
 129                        opt_connect = getenv("SMTPHOST");
 130                        if (!opt_connect)
 131                                opt_connect = (char *)"127.0.0.1";
 132                }
 133                // do connect
 134                fd = create_and_connect_stream_or_die(opt_connect, 25);
 135                // and make ourselves a simple IO filter
 136                xmove_fd(fd, STDIN_FILENO);
 137                xdup2(STDIN_FILENO, STDOUT_FILENO);
 138        }
 139        // N.B. from now we know nothing about network :)
 140
 141        // wait for initial server OK
 142        // N.B. if we used openssl the initial 220 answer is already swallowed during openssl TLS init procedure
 143        // so we need to kick the server to see whether we are ok
 144        code = smtp_check("NOOP", -1);
 145        // 220 on plain connection, 250 on openssl-helped TLS session
 146        if (220 == code)
 147                smtp_check(NULL, 250); // reread the code to stay in sync
 148        else if (250 != code)
 149                bb_error_msg_and_die("INIT failed");
 150
 151        // we should start with modern EHLO
 152        if (250 != smtp_checkp("EHLO %s", domain, -1)) {
 153                smtp_checkp("HELO %s", domain, 250);
 154        }
 155        if (ENABLE_FEATURE_CLEAN_UP)
 156                free(domain);
 157
 158        // perform authentication
 159        if (opts & OPT_a) {
 160                smtp_check("AUTH LOGIN", 334);
 161                // we must read credentials unless they are given via -a[up] options
 162                if (!G.user || !G.pass)
 163                        get_cred_or_die(4);
 164                encode_base64(NULL, G.user, NULL);
 165                smtp_check("", 334);
 166                encode_base64(NULL, G.pass, NULL);
 167                smtp_check("", 235);
 168        }
 169
 170        // set sender
 171        // N.B. we have here a very loosely defined algotythm
 172        // since sendmail historically offers no means to specify secrets on cmdline.
 173        // 1) server can require no authentication ->
 174        //      we must just provide a (possibly fake) reply address.
 175        // 2) server can require AUTH ->
 176        //      we must provide valid username and password along with a (possibly fake) reply address.
 177        //      For the sake of security username and password are to be read either from console or from a secured file.
 178        //      Since reading from console may defeat usability, the solution is either to read from a predefined
 179        //      file descriptor (e.g. 4), or again from a secured file.
 180
 181        // got no sender address? -> use system username as a resort
 182        // N.B. we marked -f as required option!
 183        //if (!G.user) {
 184        //      // N.B. IMHO getenv("USER") can be way easily spoofed!
 185        //      G.user = xuid2uname(getuid());
 186        //      opt_from = xasprintf("%s@%s", G.user, domain);
 187        //}
 188        //if (ENABLE_FEATURE_CLEAN_UP)
 189        //      free(domain);
 190        smtp_checkp("MAIL FROM:<%s>", opt_from, 250);
 191
 192        // process message
 193
 194        // read recipients from message and add them to those given on cmdline.
 195        // this means we scan stdin for To:, Cc:, Bcc: lines until an empty line
 196        // and then use the rest of stdin as message body
 197        code = 0; // set "analyze headers" mode
 198        while ((s = xmalloc_fgetline(G.fp0)) != NULL) {
 199                // put message lines doubling leading dots
 200                if (code) {
 201                        // escape leading dots
 202                        // N.B. this feature is implied even if no -i (-oi) switch given
 203                        // N.B. we need to escape the leading dot regardless of
 204                        // whether it is single or not character on the line
 205                        if ('.' == s[0] /*&& '\0' == s[1] */)
 206                                printf(".");
 207                        // dump read line
 208                        printf("%s\r\n", s);
 209                        free(s);
 210                        continue;
 211                }
 212
 213                // analyze headers
 214                // To: or Cc: headers add recipients
 215                if (0 == strncasecmp("To: ", s, 4) || 0 == strncasecmp("Bcc: " + 1, s, 4)) {
 216                        rcptto(sane_address(s+4));
 217//                      goto addh;
 218                        llist_add_to_end(&list, s);
 219                // Bcc: header adds blind copy (hidden) recipient
 220                } else if (0 == strncasecmp("Bcc: ", s, 5)) {
 221                        rcptto(sane_address(s+5));
 222                        free(s);
 223                        // N.B. Bcc: vanishes from headers!
 224                // other headers go verbatim
 225                } else if (s[0]) {
 226// addh:
 227                        llist_add_to_end(&list, s);
 228                // the empty line stops analyzing headers
 229                } else {
 230                        free(s);
 231                        // put recipients specified on cmdline
 232                        while (*argv) {
 233                                s = sane_address(*argv);
 234                                rcptto(s);
 235                                llist_add_to_end(&list, xasprintf("To: %s", s));
 236                                argv++;
 237                        }
 238                        // enter "put message" mode
 239                        smtp_check("DATA", 354);
 240                        // dump the headers
 241                        while (list) {
 242                                printf("%s\r\n", (char *) llist_pop(&list));
 243                        }
 244                        printf("%s\r\n" + 2); // quirk for format string to be reused
 245                        // stop analyzing headers
 246                        code++;
 247                }
 248        }
 249
 250        // finalize the message
 251        smtp_check(".", 250);
 252        // ... and say goodbye
 253        smtp_check("QUIT", 221);
 254        // cleanup
 255        if (ENABLE_FEATURE_CLEAN_UP)
 256                fclose(G.fp0);
 257
 258        return EXIT_SUCCESS;
 259}
 260