Initial public busybox upstream commit
[busybox4maemo] / networking / sendmail.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * bare bones sendmail/fetchmail
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
11 #define INITIAL_STDIN_FILENO 3
12
13 static void uuencode(char *fname, const char *text)
14 {
15         enum {
16                 SRC_BUF_SIZE = 45,  /* This *MUST* be a multiple of 3 */
17                 DST_BUF_SIZE = 4 * ((SRC_BUF_SIZE + 2) / 3),
18         };
19
20 #define src_buf text
21         int fd;
22 #define len fd
23         char dst_buf[DST_BUF_SIZE + 1];
24
25         if (fname) {
26                 fd = INITIAL_STDIN_FILENO;
27                 if (NOT_LONE_DASH(fname))
28                         fd = xopen(fname, O_RDONLY);
29                 src_buf = bb_common_bufsiz1;
30         // N.B. strlen(NULL) segfaults!
31         } else if (text) {
32                 // though we do not call uuencode(NULL, NULL) explicitly
33                 // still we do not want to break things suddenly
34                 len = strlen(text);
35         } else
36                 return;
37
38         fflush(stdout); // sync stdio and unistd output
39         while (1) {
40                 size_t size;
41                 if (fname) {
42                         size = full_read(fd, (char *)src_buf, SRC_BUF_SIZE);
43                         if ((ssize_t)size < 0)
44                                 bb_perror_msg_and_die(bb_msg_read_error);
45                 } else {
46                         size = len;
47                         if (len > SRC_BUF_SIZE)
48                                 size = SRC_BUF_SIZE;
49                 }
50                 if (!size)
51                         break;
52                 // encode the buffer we just read in
53                 bb_uuencode(dst_buf, src_buf, size, bb_uuenc_tbl_base64);
54                 if (fname) {
55                         xwrite(STDOUT_FILENO, "\r\n", 2);
56                 } else {
57                         src_buf += size;
58                         len -= size;
59                 }
60                 xwrite(STDOUT_FILENO, dst_buf, 4 * ((size + 2) / 3));
61         }
62         if (fname)
63                 close(fd);
64 }
65
66 struct globals {
67         pid_t helper_pid;
68         unsigned timeout;
69         // arguments for SSL connection helper
70         const char *xargs[9];
71         // arguments for postprocess helper
72         const char *fargs[3];
73 };
74 #define G (*ptr_to_globals)
75 #define helper_pid      (G.helper_pid)
76 #define timeout         (G.timeout   )
77 #define xargs           (G.xargs     )
78 #define fargs           (G.fargs     )
79 #define INIT_G() do { \
80         SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
81         xargs[0] = "openssl"; \
82         xargs[1] = "s_client"; \
83         xargs[2] = "-quiet"; \
84         xargs[3] = "-connect"; \
85         /*xargs[4] = "server[:port]";*/ \
86         xargs[5] = "-tls1"; \
87         xargs[6] = "-starttls"; \
88         xargs[7] = "smtp"; \
89         fargs[0] = "utf-8"; \
90 } while (0)
91
92 #define opt_connect       (xargs[4])
93 #define opt_after_connect (xargs[5])
94 #define opt_charset       (fargs[0])
95 #define opt_subject       (fargs[1])
96
97 static void kill_helper(void)
98 {
99         // TODO!!!: is there more elegant way to terminate child on program failure?
100         if (helper_pid > 0)
101                 kill(helper_pid, SIGTERM);
102 }
103
104 // generic signal handler
105 static void signal_handler(int signo)
106 {
107 #define err signo
108
109         if (SIGALRM == signo) {
110                 kill_helper();
111                 bb_error_msg_and_die("timed out");
112         }
113
114         // SIGCHLD. reap zombies
115         if (wait_any_nohang(&err) > 0)
116                 if (WIFEXITED(err) && WEXITSTATUS(err))
117                         bb_error_msg_and_die("child exited (%d)", WEXITSTATUS(err));
118 }
119
120 static void launch_helper(const char **argv)
121 {
122         // setup vanilla unidirectional pipes interchange
123         int idx;
124         int pipes[4];
125         xpipe(pipes);
126         xpipe(pipes+2);
127         helper_pid = vfork();
128         if (helper_pid < 0)
129                 bb_perror_msg_and_die("vfork");
130         idx = (!helper_pid)*2;
131         xdup2(pipes[idx], STDIN_FILENO);
132         xdup2(pipes[3-idx], STDOUT_FILENO);
133         if (ENABLE_FEATURE_CLEAN_UP)
134                 for (int i = 4; --i >= 0; )
135                         if (pipes[i] > STDOUT_FILENO)
136                                 close(pipes[i]);
137         if (!helper_pid) {
138                 // child: try to execute connection helper
139                 BB_EXECVP(*argv, (char **)argv);
140                 _exit(127);
141         }
142         // parent: check whether child is alive
143         bb_signals(0
144                 + (1 << SIGCHLD)
145                 + (1 << SIGALRM)
146                 , signal_handler);
147         signal_handler(SIGCHLD);
148         // child seems OK -> parent goes on
149 }
150
151 static const char *command(const char *fmt, const char *param)
152 {
153         const char *msg = fmt;
154         alarm(timeout);
155         if (msg) {
156                 msg = xasprintf(fmt, param);
157                 printf("%s\r\n", msg);
158         }
159         fflush(stdout);
160         return msg;
161 }
162
163 static int smtp_checkp(const char *fmt, const char *param, int code)
164 {
165         char *answer;
166         const char *msg = command(fmt, param);
167         // read stdin
168         // if the string has a form \d\d\d- -- read next string. E.g. EHLO response
169         // parse first bytes to a number
170         // if code = -1 then just return this number
171         // if code != -1 then checks whether the number equals the code
172         // if not equal -> die saying msg
173         while ((answer = xmalloc_getline(stdin)) != NULL)
174                 if (strlen(answer) <= 3 || '-' != answer[3])
175                         break;
176         if (answer) {
177                 int n = atoi(answer);
178                 alarm(0);
179                 if (ENABLE_FEATURE_CLEAN_UP) {
180                         free(answer);
181                 }
182                 if (-1 == code || n == code) {
183                         return n;
184                 }
185         }
186         kill_helper();
187         bb_error_msg_and_die("%s failed", msg);
188 }
189
190 static int inline smtp_check(const char *fmt, int code)
191 {
192         return smtp_checkp(fmt, NULL, code);
193 }
194
195 // strip argument of bad chars
196 static char *sane(char *str)
197 {
198         char *s = str;
199         char *p = s;
200         while (*s) {
201                 if (isalnum(*s) || '_' == *s || '-' == *s || '.' == *s || '@' == *s) {
202                         *p++ = *s;
203                 }
204                 s++;
205         }
206         *p = '\0';
207         return str;
208 }
209
210 #if ENABLE_FETCHMAIL
211 static void pop3_checkr(const char *fmt, const char *param, char **ret)
212 {
213         const char *msg = command(fmt, param);
214         char *answer = xmalloc_getline(stdin);
215         if (answer && '+' == *answer) {
216                 alarm(0);
217                 if (ret)
218                         *ret = answer+4; // skip "+OK "
219                 else if (ENABLE_FEATURE_CLEAN_UP)
220                         free(answer);
221                 return;
222         }
223         kill_helper();
224         bb_error_msg_and_die("%s failed", msg);
225 }
226
227 static void inline pop3_check(const char *fmt, const char *param)
228 {
229         pop3_checkr(fmt, param, NULL);
230 }
231
232 static void pop3_message(const char *filename)
233 {
234         int fd;
235         char *answer;
236         // create and open file filename
237         // read stdin, copy to created file
238         fd = xopen(filename, O_CREAT | O_WRONLY | O_TRUNC | O_EXCL);
239         while ((answer = xmalloc_fgets_str(stdin, "\r\n")) != NULL) {
240                 char *s = answer;
241                 if ('.' == *answer) {
242                         if ('.' == answer[1])
243                                 s++;
244                         else if ('\r' == answer[1] && '\n' == answer[2] && '\0' == answer[3])
245                                 break;
246                 }
247                 xwrite(fd, s, strlen(s));
248                 free(answer);
249         }
250         close(fd);
251 }
252 #endif
253
254 int sendgetmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
255 int sendgetmail_main(int argc ATTRIBUTE_UNUSED, char **argv)
256 {
257         llist_t *opt_recipients = NULL;
258
259         const char *opt_user;
260         const char *opt_pass;
261
262         enum {
263                 OPT_w = 1 << 0,         // network timeout
264                 OPT_U = 1 << 1,         // user
265                 OPT_P = 1 << 2,         // password
266                 OPT_X = 1 << 3,         // connect using openssl s_client helper
267
268                 OPTS_n = 1 << 4,        // sendmail: request notification
269                 OPTF_t = 1 << 4,        // fetchmail: use "TOP" not "RETR"
270
271                 OPTS_s = 1 << 5,        // sendmail: subject
272                 OPTF_z = 1 << 5,        // fetchmail: delete from server
273
274                 OPTS_c = 1 << 6,        // sendmail: assumed charset
275                 OPTS_t = 1 << 7,        // sendmail: recipient(s)
276         };
277
278         const char *options;
279         unsigned opts;
280
281         // init global variables
282         INIT_G();
283
284         // parse options, different option sets for sendmail and fetchmail
285         // N.B. opt_after_connect hereafter is NULL if we are called as fetchmail
286         // and is NOT NULL if we are called as sendmail
287         if (!ENABLE_FETCHMAIL || 's' == applet_name[0]) {
288                 // SENDMAIL
289                 // save initial stdin (body or attachements can be piped!)
290                 xdup2(STDIN_FILENO, INITIAL_STDIN_FILENO);
291                 opt_complementary = "-2:w+:t:t::"; // count(-t) > 0
292                 options = "w:U:P:X" "ns:c:t:";
293         } else {
294                 // FETCHMAIL
295                 opt_after_connect = NULL;
296                 opt_complementary = "-2:w+:P";
297                 options = "w:U:P:X" "tz";
298         }
299         opts = getopt32(argv, options,
300                 &timeout, &opt_user, &opt_pass,
301                 &opt_subject, &opt_charset, &opt_recipients
302         );
303         //argc -= optind;
304         argv += optind;
305
306         // first argument is remote server[:port]
307         opt_connect = *argv++;
308
309         // connect to server
310         // SSL ordered? ->
311         if (opts & OPT_X) {
312                 // ... use openssl helper
313                 launch_helper(xargs);
314         // no SSL ordered? ->
315         } else {
316                 // ... make plain connect
317                 int fd = create_and_connect_stream_or_die(opt_connect, 25);
318                 // make ourselves a simple IO filter
319                 // from now we know nothing about network :)
320                 xmove_fd(fd, STDIN_FILENO);
321                 xdup2(STDIN_FILENO, STDOUT_FILENO);
322         }
323
324 #if ENABLE_FETCHMAIL
325         // we are sendmail?
326         if (opt_after_connect)
327 #endif
328         {
329 /***************************************************
330  * SENDMAIL
331  ***************************************************/
332
333                 char *opt_from;
334                 int code;
335                 char *boundary;
336                 const char *fmt;
337                 const char *p;
338                 char *q;
339
340                 // we didn't use SSL helper? ->
341                 if (!(opts & OPT_X)) {
342                         // ... wait for initial server OK
343                         smtp_check(NULL, 220);
344                 }
345
346                 // get the sender
347                 opt_from = sane(*argv++);
348
349                 // introduce to server
350                 // we should start with modern EHLO
351                 if (250 != smtp_checkp("EHLO %s", opt_from, -1)) {
352                         smtp_checkp("HELO %s", opt_from, 250);
353                 }
354
355                 // set sender
356                 // NOTE: if password has not been specified
357                 // then no authentication is possible
358                 code = (opts & OPT_P) ? -1 : 250;
359                 // first try softly without authentication
360                 while (250 != smtp_checkp("MAIL FROM:<%s>", opt_from, code)) {
361                         // MAIL FROM failed -> authentication needed
362                         // have we got username?
363                         if (!(opts & OPT_U)) {
364                                 // no! fetch it from "from" option
365                                 //opts |= OPT_U;
366                                 opt_user = xstrdup(opt_from);
367                                 *strchrnul(opt_user, '@') = '\0';
368                         }
369                         // now we've got username
370                         // so try to authenticate
371                         if (334 == smtp_check("AUTH LOGIN", -1)) {
372                                 uuencode(NULL, opt_user);
373                                 smtp_check("", 334);
374                                 uuencode(NULL, opt_pass);
375                                 smtp_check("", 235);
376                         }
377                         // authenticated OK? -> retry to set sender
378                         // but this time die on failure!
379                         code = 250;
380                 }
381
382                 // set recipients
383                 for (llist_t *to = opt_recipients; to; to = to->link) {
384                         smtp_checkp("RCPT TO:<%s>", sane(to->data), 250);
385                 }
386
387                 // enter "put message" mode
388                 smtp_check("DATA", 354);
389
390                 // put address headers
391                 printf("From: %s\r\n", opt_from);
392                 for (llist_t *to = opt_recipients; to; to = to->link) {
393                         printf("To: %s\r\n", to->data);
394                 }
395
396                 // put encoded subject
397                 if (opts & OPTS_c)
398                         sane((char *)opt_charset);
399                 if (opts & OPTS_s) {
400                         printf("Subject: =?%s?B?", opt_charset);
401                         uuencode(NULL, opt_subject);
402                         printf("?=\r\n");
403                 }
404
405                 // put notification
406                 if (opts & OPTS_n)
407                         printf("Disposition-Notification-To: %s\r\n", opt_from);
408
409                 // make a random string -- it will delimit message parts
410                 srand(monotonic_us());
411                 boundary = xasprintf("%d-%d-%d", rand(), rand(), rand());
412
413                 // put common headers and body start
414                 printf(
415                         "Message-ID: <%s>\r\n"
416                         "Mime-Version: 1.0\r\n"
417                         "%smultipart/mixed; boundary=\"%s\"\r\n"
418                         , boundary
419                         , "Content-Type: "
420                         , boundary
421                 );
422
423                 // put body + attachment(s)
424                 // N.B. all these weird things just to be tiny
425                 // by reusing string patterns!
426                 fmt =
427                         "\r\n--%s\r\n"
428                         "%stext/plain; charset=%s\r\n"
429                         "%s%s\r\n"
430                         "%s"
431                 ;
432                 p = opt_charset;
433                 q = (char *)"";
434                 while (*argv) {
435                         printf(
436                                 fmt
437                                 , boundary
438                                 , "Content-Type: "
439                                 , p
440                                 , "Content-Disposition: inline"
441                                 , q
442                                 , "Content-Transfer-Encoding: base64\r\n"
443                         );
444                         p = "";
445                         fmt =
446                                 "\r\n--%s\r\n"
447                                 "%sapplication/octet-stream%s\r\n"
448                                 "%s; filename=\"%s\"\r\n"
449                                 "%s"
450                         ;
451                         uuencode(*argv, NULL);
452                         if (*(++argv))
453                                 q = bb_get_last_path_component_strip(*argv);
454                 }
455
456                 // put message terminator
457                 printf("\r\n--%s--\r\n" "\r\n", boundary);
458
459                 // leave "put message" mode
460                 smtp_check(".", 250);
461                 // ... and say goodbye
462                 smtp_check("QUIT", 221);
463
464 #if ENABLE_FETCHMAIL
465         } else {
466 /***************************************************
467  * FETCHMAIL
468  ***************************************************/
469
470                 char *buf;
471                 unsigned nmsg;
472                 char *hostname;
473                 pid_t pid;
474
475                 // cache fetch command:
476                 // TOP will return only the headers
477                 // RETR will dump the whole message
478                 const char *retr = (opts & OPTF_t) ? "TOP %u 0" : "RETR %u";
479
480                 // goto maildir
481                 xchdir(*argv++);
482
483                 // cache postprocess program
484                 *fargs = *argv;
485                 
486                 // authenticate
487                 if (!(opts & OPT_U)) {
488                         //opts |= OPT_U;
489                         // N.B. IMHO getenv("USER") can be way easily spoofed!
490                         opt_user = bb_getpwuid(NULL, -1, getuid());
491                 }
492
493                 // get server greeting
494                 pop3_checkr(NULL, NULL, &buf);
495
496                 // server supports APOP?
497                 if ('<' == *buf) {
498                         md5_ctx_t md5;
499                         // yes! compose <stamp><password>
500                         char *s = strchr(buf, '>');
501                         if (s)
502                                 strcpy(s+1, opt_pass);
503                         s = buf;
504                         // get md5 sum of <stamp><password>
505                         md5_begin(&md5);
506                         md5_hash(s, strlen(s), &md5);
507                         md5_end(s, &md5);
508                         // NOTE: md5 struct contains enough space
509                         // so we reuse md5 space instead of xzalloc(16*2+1)
510 #define md5_hex ((uint8_t *)&md5)
511 //                      uint8_t *md5_hex = (uint8_t *)&md5;
512                         *bin2hex(md5_hex, s, 16) = '\0';
513                         // APOP
514                         s = xasprintf("%s %s", opt_user, md5_hex);
515 #undef md5_hex
516                         pop3_check("APOP %s", s);
517                         if (ENABLE_FEATURE_CLEAN_UP) {
518                                 free(s);
519                                 free(buf-4); // buf is "+OK " away from malloc'ed string
520                         }
521                 // server ignores APOP -> use simple text authentication
522                 } else {
523                         // USER
524                         pop3_check("USER %s", opt_user);
525                         // PASS
526                         pop3_check("PASS %s", opt_pass);
527                 }
528
529                 // get mailbox statistics
530                 pop3_checkr("STAT", NULL, &buf);
531
532                 // prepare message filename suffix
533                 hostname = safe_gethostname();
534                 pid = getpid();
535
536                 // get messages counter
537                 // NOTE: we don't use xatou(buf) since buf is "nmsg nbytes"
538                 // we only need nmsg and atoi is just exactly what we need
539                 // if atoi fails to convert buf into number it returns 0
540                 // in this case the following loop simply will not be executed 
541                 nmsg = atoi(buf);
542                 if (ENABLE_FEATURE_CLEAN_UP)
543                         free(buf-4); // buf is "+OK " away from malloc'ed string
544
545                 // loop through messages
546                 for (; nmsg; nmsg--) {
547
548                         // generate unique filename
549                         char *filename = xasprintf("tmp/%llu.%u.%s", monotonic_us(), pid, hostname);
550                         char *target;
551                         int rc;
552
553                         // retrieve message in ./tmp/
554                         pop3_check(retr, (const char *)(ptrdiff_t)nmsg);
555                         pop3_message(filename);
556                         // delete message from server
557                         if (opts & OPTF_z)
558                                 pop3_check("DELE %u", (const char*)(ptrdiff_t)nmsg);
559
560                         // run postprocessing program
561                         if (*fargs) {
562                                 fargs[1] = filename;
563                                 rc = wait4pid(spawn((char **)fargs));
564                                 if (99 == rc)
565                                         break;
566                                 if (1 == rc)
567                                         goto skip;
568                         }
569
570                         // atomically move message to ./new/
571                         target = xstrdup(filename);
572                         strncpy(target, "new", 3);
573                         // ... or just stop receiving on error
574                         if (rename_or_warn(filename, target))
575                                 break;
576                         free(target);
577  skip:
578                         free(filename);
579                 }
580
581                 // Bye
582                 pop3_check("QUIT", NULL);
583 #endif // ENABLE_FETCHMAIL
584         }
585
586         return 0;
587 }