source: MondoRescue/branches/3.3/mindi-busybox/mailutils/sendmail.c@ 3723

Last change on this file since 3723 was 3621, checked in by Bruno Cornec, 10 years ago

New 3?3 banch for incorporation of latest busybox 1.25. Changing minor version to handle potential incompatibilities.

  • Property svn:eol-style set to native
File size: 14.3 KB
Line 
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_error_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 bb_putchar('.');
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}
Note: See TracBrowser for help on using the repository browser.