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