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