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 source tree. 8 */ 9 10//kbuild:lib-$(CONFIG_SENDMAIL) += sendmail.o mail.o 11 12//usage:#define sendmail_trivial_usage 13//usage: "[OPTIONS] [RECIPIENT_EMAIL]..." 14//usage:#define sendmail_full_usage "\n\n" 15//usage: "Read email from stdin and send it\n" 16//usage: "\nStandard options:" 17//usage: "\n -t Read additional recipients from message body" 18//usage: "\n -f SENDER For use in MAIL FROM:<sender>. Can be empty string" 19//usage: "\n Default: -auUSER, or username of current UID" 20//usage: "\n -o OPTIONS Various options. -oi implied, others are ignored" 21//usage: "\n -i -oi synonym. implied and ignored" 22//usage: "\n" 23//usage: "\nBusybox specific options:" 24//usage: "\n -v Verbose" 25//usage: "\n -w SECS Network timeout" 26//usage: "\n -H 'PROG ARGS' Run connection helper" 27//usage: "\n Examples:" 28//usage: "\n -H 'exec openssl s_client -quiet -tls1 -starttls smtp" 29//usage: "\n -connect smtp.gmail.com:25' <email.txt" 30//usage: "\n [4<username_and_passwd.txt | -auUSER -apPASS]" 31//usage: "\n -H 'exec openssl s_client -quiet -tls1" 32//usage: "\n -connect smtp.gmail.com:465' <email.txt" 33//usage: "\n [4<username_and_passwd.txt | -auUSER -apPASS]" 34//usage: "\n -S HOST[:PORT] Server" 35//usage: "\n -auUSER Username for AUTH LOGIN" 36//usage: "\n -apPASS Password for AUTH LOGIN" 37////usage: "\n -amMETHOD Authentication method. Ignored. LOGIN is implied" 38//usage: "\n" 39//usage: "\nOther options are silently ignored; -oi -t is implied" 40//usage: IF_MAKEMIME( 41//usage: "\nUse makemime to create emails with attachments" 42//usage: ) 43 44/* Currently we don't sanitize or escape user-supplied SENDER and RECIPIENT_EMAILs. 45 * We may need to do so. For one, '.' in usernames seems to require escaping! 46 * 47 * From http://cr.yp.to/smtp/address.html: 48 * 49 * SMTP offers three ways to encode a character inside an address: 50 * 51 * "safe": the character, if it is not <>()[].,;:@, backslash, 52 * double-quote, space, or an ASCII control character; 53 * "quoted": the character, if it is not \012, \015, backslash, 54 * or double-quote; or 55 * "slashed": backslash followed by the character. 56 * 57 * An encoded box part is either (1) a sequence of one or more slashed 58 * or safe characters or (2) a double quote, a sequence of zero or more 59 * slashed or quoted characters, and a double quote. It represents 60 * the concatenation of the characters encoded inside it. 61 * 62 * For example, the encoded box parts 63 * angels 64 * \a\n\g\e\l\s 65 * "\a\n\g\e\l\s" 66 * "angels" 67 * "ang\els" 68 * all represent the 6-byte string "angels", and the encoded box parts 69 * a\,comma 70 * \a\,\c\o\m\m\a 71 * "a,comma" 72 * all represent the 7-byte string "a,comma". 73 * 74 * An encoded address contains 75 * the byte <; 76 * optionally, a route followed by a colon; 77 * an encoded box part, the byte @, and a domain; and 78 * the byte >. 79 * 80 * It represents an Internet mail address, given by concatenating 81 * the string represented by the encoded box part, the byte @, 82 * and the domain. For example, the encoded addresses 83 * <God@heaven.af.mil> 84 * <\God@heaven.af.mil> 85 * <"God"@heaven.af.mil> 86 * <@gateway.af.mil,@uucp.local:"\G\o\d"@heaven.af.mil> 87 * all represent the Internet mail address "God@heaven.af.mil". 88 */ 89 90#include "libbb.h" 91#include "mail.h" 92 93// limit maximum allowed number of headers to prevent overflows. 94// set to 0 to not limit 95#define MAX_HEADERS 256 96 97static void send_r_n(const char *s) 98{ 99 if (verbose) 100 bb_error_msg("send:'%s'", s); 101 printf("%s\r\n", s); 102} 103 104static int smtp_checkp(const char *fmt, const char *param, int code) 105{ 106 char *answer; 107 char *msg = send_mail_command(fmt, param); 108 // read stdin 109 // if the string has a form NNN- -- read next string. E.g. EHLO response 110 // parse first bytes to a number 111 // if code = -1 then just return this number 112 // if code != -1 then checks whether the number equals the code 113 // if not equal -> die saying msg 114 while ((answer = xmalloc_fgetline(stdin)) != NULL) { 115 if (verbose) 116 bb_error_msg("recv:'%.*s'", (int)(strchrnul(answer, '\r') - answer), answer); 117 if (strlen(answer) <= 3 || '-' != answer[3]) 118 break; 119 free(answer); 120 } 121 if (answer) { 122 int n = atoi(answer); 123 if (timeout) 124 alarm(0); 125 free(answer); 126 if (-1 == code || n == code) { 127 free(msg); 128 return n; 129 } 130 } 131 bb_error_msg_and_die("%s failed", msg); 132} 133 134static int smtp_check(const char *fmt, int code) 135{ 136 return smtp_checkp(fmt, NULL, code); 137} 138 139// strip argument of bad chars 140static char *sane_address(char *str) 141{ 142 char *s; 143 144 trim(str); 145 s = str; 146 while (*s) { 147 if (!isalnum(*s) && !strchr("_-.@", *s)) { 148 bb_error_msg("bad address '%s'", str); 149 /* returning "": */ 150 str[0] = '\0'; 151 return str; 152 } 153 s++; 154 } 155 return str; 156} 157 158// check for an address inside angle brackets, if not found fall back to normal 159static char *angle_address(char *str) 160{ 161 char *s, *e; 162 163 trim(str); 164 e = last_char_is(str, '>'); 165 if (e) { 166 s = strrchr(str, '<'); 167 if (s) { 168 *e = '\0'; 169 str = s + 1; 170 } 171 } 172 return sane_address(str); 173} 174 175static void rcptto(const char *s) 176{ 177 if (!*s) 178 return; 179 // N.B. we don't die if recipient is rejected, for the other recipients may be accepted 180 if (250 != smtp_checkp("RCPT TO:<%s>", s, -1)) 181 bb_error_msg("Bad recipient: <%s>", s); 182} 183 184// send to a list of comma separated addresses 185static void rcptto_list(const char *list) 186{ 187 char *str = xstrdup(list); 188 char *s = str; 189 char prev = 0; 190 int in_quote = 0; 191 192 while (*s) { 193 char ch = *s++; 194 195 if (ch == '"' && prev != '\\') { 196 in_quote = !in_quote; 197 } else if (!in_quote && ch == ',') { 198 s[-1] = '\0'; 199 rcptto(angle_address(str)); 200 str = s; 201 } 202 prev = ch; 203 } 204 if (prev != ',') 205 rcptto(angle_address(str)); 206 free(str); 207} 208 209int sendmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; 210int sendmail_main(int argc UNUSED_PARAM, char **argv) 211{ 212 char *opt_connect = opt_connect; 213 char *opt_from = NULL; 214 char *s; 215 llist_t *list = NULL; 216 char *host = sane_address(safe_gethostname()); 217 unsigned nheaders = 0; 218 int code; 219 enum { 220 HDR_OTHER = 0, 221 HDR_TOCC, 222 HDR_BCC, 223 } last_hdr = 0; 224 int check_hdr; 225 int has_to = 0; 226 227 enum { 228 //--- standard options 229 OPT_t = 1 << 0, // read message for recipients, append them to those on cmdline 230 OPT_f = 1 << 1, // sender address 231 OPT_o = 1 << 2, // various options. -oi IMPLIED! others are IGNORED! 232 OPT_i = 1 << 3, // IMPLIED! 233 //--- BB specific options 234 OPT_w = 1 << 4, // network timeout 235 OPT_H = 1 << 5, // use external connection helper 236 OPT_S = 1 << 6, // specify connection string 237 OPT_a = 1 << 7, // authentication tokens 238 OPT_v = 1 << 8, // verbosity 239 }; 240 241 // init global variables 242 INIT_G(); 243 244 // save initial stdin since body is piped! 245 xdup2(STDIN_FILENO, 3); 246 G.fp0 = xfdopen_for_read(3); 247 248 // parse options 249 // -v is a counter, -H and -S are mutually exclusive, -a is a list 250 opt_complementary = "vv:w+:H--S:S--H:a::"; 251 // N.B. since -H and -S are mutually exclusive they do not interfere in opt_connect 252 // -a is for ssmtp (http://downloads.openwrt.org/people/nico/man/man8/ssmtp.8.html) compatibility, 253 // it is still under development. 254 opts = getopt32(argv, "tf:o:iw:H:S:a::v", &opt_from, NULL, 255 &timeout, &opt_connect, &opt_connect, &list, &verbose); 256 //argc -= optind; 257 argv += optind; 258 259 // process -a[upm]<token> options 260 if ((opts & OPT_a) && !list) 261 bb_show_usage(); 262 while (list) { 263 char *a = (char *) llist_pop(&list); 264 if ('u' == a[0]) 265 G.user = xstrdup(a+1); 266 if ('p' == a[0]) 267 G.pass = xstrdup(a+1); 268 // N.B. we support only AUTH LOGIN so far 269 //if ('m' == a[0]) 270 // G.method = xstrdup(a+1); 271 } 272 // N.B. list == NULL here 273 //bb_info_msg("OPT[%x] AU[%s], AP[%s], AM[%s], ARGV[%s]", opts, au, ap, am, *argv); 274 275 // connect to server 276 277 // connection helper ordered? -> 278 if (opts & OPT_H) { 279 const char *args[] = { "sh", "-c", opt_connect, NULL }; 280 // plug it in 281 launch_helper(args); 282 // Now: 283 // our stdout will go to helper's stdin, 284 // helper's stdout will be available on our stdin. 285 286 // Wait for initial server message. 287 // If helper (such as openssl) invokes STARTTLS, the initial 220 288 // is swallowed by helper (and not repeated after TLS is initiated). 289 // We will send NOOP cmd to server and check the response. 290 // We should get 220+250 on plain connection, 250 on STARTTLSed session. 291 // 292 // The problem here is some servers delay initial 220 message, 293 // and consider client to be a spammer if it starts sending cmds 294 // before 220 reached it. The code below is unsafe in this regard: 295 // in non-STARTTLSed case, we potentially send NOOP before 220 296 // is sent by server. 297 // Ideas? (--delay SECS opt? --assume-starttls-helper opt?) 298 code = smtp_check("NOOP", -1); 299 if (code == 220) 300 // we got 220 - this is not STARTTLSed connection, 301 // eat 250 response to our NOOP 302 smtp_check(NULL, 250); 303 else 304 if (code != 250) 305 bb_error_msg_and_die("SMTP init failed"); 306 } else { 307 // vanilla connection 308 int fd; 309 // host[:port] not explicitly specified? -> use $SMTPHOST 310 // no $SMTPHOST? -> use localhost 311 if (!(opts & OPT_S)) { 312 opt_connect = getenv("SMTPHOST"); 313 if (!opt_connect) 314 opt_connect = (char *)"127.0.0.1"; 315 } 316 // do connect 317 fd = create_and_connect_stream_or_die(opt_connect, 25); 318 // and make ourselves a simple IO filter 319 xmove_fd(fd, STDIN_FILENO); 320 xdup2(STDIN_FILENO, STDOUT_FILENO); 321 322 // Wait for initial server 220 message 323 smtp_check(NULL, 220); 324 } 325 326 // we should start with modern EHLO 327 if (250 != smtp_checkp("EHLO %s", host, -1)) 328 smtp_checkp("HELO %s", host, 250); 329 330 // perform authentication 331 if (opts & OPT_a) { 332 smtp_check("AUTH LOGIN", 334); 333 // we must read credentials unless they are given via -a[up] options 334 if (!G.user || !G.pass) 335 get_cred_or_die(4); 336 encode_base64(NULL, G.user, NULL); 337 smtp_check("", 334); 338 encode_base64(NULL, G.pass, NULL); 339 smtp_check("", 235); 340 } 341 342 // set sender 343 // N.B. we have here a very loosely defined algorythm 344 // since sendmail historically offers no means to specify secrets on cmdline. 345 // 1) server can require no authentication -> 346 // we must just provide a (possibly fake) reply address. 347 // 2) server can require AUTH -> 348 // we must provide valid username and password along with a (possibly fake) reply address. 349 // For the sake of security username and password are to be read either from console or from a secured file. 350 // Since reading from console may defeat usability, the solution is either to read from a predefined 351 // file descriptor (e.g. 4), or again from a secured file. 352 353 // got no sender address? use auth name, then UID username as a last resort 354 if (!opt_from) { 355 opt_from = xasprintf("%s@%s", 356 G.user ? G.user : xuid2uname(getuid()), 357 xgethostbyname(host)->h_name); 358 } 359 free(host); 360 361 smtp_checkp("MAIL FROM:<%s>", opt_from, 250); 362 363 // process message 364 365 // read recipients from message and add them to those given on cmdline. 366 // this means we scan stdin for To:, Cc:, Bcc: lines until an empty line 367 // and then use the rest of stdin as message body 368 code = 0; // set "analyze headers" mode 369 while ((s = xmalloc_fgetline(G.fp0)) != NULL) { 370 dump: 371 // put message lines doubling leading dots 372 if (code) { 373 // escape leading dots 374 // N.B. this feature is implied even if no -i (-oi) switch given 375 // N.B. we need to escape the leading dot regardless of 376 // whether it is single or not character on the line 377 if ('.' == s[0] /*&& '\0' == s[1] */) 378 printf("."); 379 // dump read line 380 send_r_n(s); 381 free(s); 382 continue; 383 } 384 385 // analyze headers 386 // To: or Cc: headers add recipients 387 check_hdr = (0 == strncasecmp("To:", s, 3)); 388 has_to |= check_hdr; 389 if (opts & OPT_t) { 390 if (check_hdr || 0 == strncasecmp("Bcc:" + 1, s, 3)) { 391 rcptto_list(s+3); 392 last_hdr = HDR_TOCC; 393 goto addheader; 394 } 395 // Bcc: header adds blind copy (hidden) recipient 396 if (0 == strncasecmp("Bcc:", s, 4)) { 397 rcptto_list(s+4); 398 free(s); 399 last_hdr = HDR_BCC; 400 continue; // N.B. Bcc: vanishes from headers! 401 } 402 } 403 check_hdr = (list && isspace(s[0])); 404 if (strchr(s, ':') || check_hdr) { 405 // other headers go verbatim 406 // N.B. RFC2822 2.2.3 "Long Header Fields" allows for headers to occupy several lines. 407 // Continuation is denoted by prefixing additional lines with whitespace(s). 408 // Thanks (stefan.seyfried at googlemail.com) for pointing this out. 409 if (check_hdr && last_hdr != HDR_OTHER) { 410 rcptto_list(s+1); 411 if (last_hdr == HDR_BCC) 412 continue; 413 // N.B. Bcc: vanishes from headers! 414 } else { 415 last_hdr = HDR_OTHER; 416 } 417 addheader: 418 // N.B. we allow MAX_HEADERS generic headers at most to prevent attacks 419 if (MAX_HEADERS && ++nheaders >= MAX_HEADERS) 420 goto bail; 421 llist_add_to_end(&list, s); 422 } else { 423 // a line without ":" (an empty line too, by definition) doesn't look like a valid header 424 // so stop "analyze headers" mode 425 reenter: 426 // put recipients specified on cmdline 427 check_hdr = 1; 428 while (*argv) { 429 char *t = sane_address(*argv); 430 rcptto(t); 431 //if (MAX_HEADERS && ++nheaders >= MAX_HEADERS) 432 // goto bail; 433 if (!has_to) { 434 const char *hdr; 435 436 if (check_hdr && argv[1]) 437 hdr = "To: %s,"; 438 else if (check_hdr) 439 hdr = "To: %s"; 440 else if (argv[1]) 441 hdr = "To: %s," + 3; 442 else 443 hdr = "To: %s" + 3; 444 llist_add_to_end(&list, 445 xasprintf(hdr, t)); 446 check_hdr = 0; 447 } 448 argv++; 449 } 450 // enter "put message" mode 451 // N.B. DATA fails iff no recipients were accepted (or even provided) 452 // in this case just bail out gracefully 453 if (354 != smtp_check("DATA", -1)) 454 goto bail; 455 // dump the headers 456 while (list) { 457 send_r_n((char *) llist_pop(&list)); 458 } 459 // stop analyzing headers 460 code++; 461 // N.B. !s means: we read nothing, and nothing to be read in the future. 462 // just dump empty line and break the loop 463 if (!s) { 464 send_r_n(""); 465 break; 466 } 467 // go dump message body 468 // N.B. "s" already contains the first non-header line, so pretend we read it from input 469 goto dump; 470 } 471 } 472 // odd case: we didn't stop "analyze headers" mode -> message body is empty. Reenter the loop 473 // N.B. after reenter code will be > 0 474 if (!code) 475 goto reenter; 476 477 // finalize the message 478 smtp_check(".", 250); 479 bail: 480 // ... and say goodbye 481 smtp_check("QUIT", 221); 482 // cleanup 483 if (ENABLE_FEATURE_CLEAN_UP) 484 fclose(G.fp0); 485 486 return EXIT_SUCCESS; 487} 488