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