libbb: introduce and use xrename and rename_or_warn.
[oweals/busybox.git] / 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         } else {
31                 len = strlen(text);
32         }
33
34         fflush(stdout); // sync stdio and unistd output
35         while (1) {
36                 size_t size;
37                 if (fname) {
38                         size = full_read(fd, (char *)src_buf, SRC_BUF_SIZE);
39                         if ((ssize_t)size < 0)
40                                 bb_perror_msg_and_die(bb_msg_read_error);
41                 } else {
42                         size = len;
43                         if (len > SRC_BUF_SIZE)
44                                 size = SRC_BUF_SIZE;
45                 }
46                 if (!size)
47                         break;
48                 // encode the buffer we just read in
49                 bb_uuencode(dst_buf, src_buf, size, bb_uuenc_tbl_base64);
50                 if (fname) {
51                         xwrite(STDOUT_FILENO, "\r\n", 2);
52                 } else {
53                         src_buf += size;
54                         len -= size;
55                 }
56                 xwrite(STDOUT_FILENO, dst_buf, 4 * ((size + 2) / 3));
57         }
58         if (fname)
59                 close(fd);
60 }
61
62 static pid_t helper_pid;
63
64 static void kill_helper(void)
65 {
66         // TODO!!!: is there more elegant way to terminate child on program failure?
67         if (helper_pid > 0)
68                 kill(helper_pid, SIGTERM);
69 }
70
71 // generic signal handler
72 static void signal_handler(int signo)
73 {
74         int err;
75
76         if (SIGALRM == signo) {
77                 kill_helper();
78                 bb_error_msg_and_die("timed out");
79         }
80
81         // SIGCHLD. reap zombies
82         if (wait_any_nohang(&err) > 0)
83                 if (WIFEXITED(err) && WEXITSTATUS(err))
84 #if ENABLE_FEATURE_SENDMAIL_BLOATY
85                         bb_error_msg_and_die("child exited (%d)", WEXITSTATUS(err));
86 #else
87                         bb_error_msg_and_die("child failed");
88 #endif
89 }
90
91 static void launch_helper(const char **argv)
92 {
93         // setup vanilla unidirectional pipes interchange
94         int idx;
95         int pipes[4];
96         xpipe(pipes);
97         xpipe(pipes+2);
98         helper_pid = vfork();
99         if (helper_pid < 0)
100                 bb_perror_msg_and_die("vfork");
101         idx = (!helper_pid)*2;
102         xdup2(pipes[idx], STDIN_FILENO);
103         xdup2(pipes[3-idx], STDOUT_FILENO);
104         if (ENABLE_FEATURE_CLEAN_UP)
105                 for (int i = 4; --i >= 0; )
106                         if (pipes[i] > STDOUT_FILENO)
107                                 close(pipes[i]);
108         if (!helper_pid) {
109                 // child - try to execute connection helper
110                 BB_EXECVP(argv[0], (char **)argv);
111                 _exit(127);
112         }
113         // parent - check whether child is alive
114         bb_signals_recursive(0
115                         + (1 << SIGCHLD)
116                         + (1 << SIGALRM)
117                         , signal_handler);
118         signal_handler(SIGCHLD);
119         // child seems OK -> parent goes on
120 }
121
122 static unsigned timeout;
123
124 static char *command(const char *fmt, const char *param)
125 {
126         char *msg = (char *)fmt;
127         alarm(timeout);
128         if (msg) {
129 //              if (param)
130                         msg = xasprintf(fmt, param);
131                 printf("%s\r\n", msg);
132         }
133         fflush(stdout);
134         return msg;
135 }
136
137 static int smtp_checkp(const char *fmt, const char *param, int code)
138 {
139         char *answer;
140         char *msg = command(fmt, param);
141         // read stdin
142         // if the string has a form \d\d\d- -- read next string. E.g. EHLO response
143         // parse first bytes to a number
144         // if code = -1 then just return this number
145         // if code != -1 then checks whether the number equals the code
146         // if not equal -> die saying msg
147 #if ENABLE_FEATURE_SENDMAIL_EHLO
148         while ((answer = xmalloc_getline(stdin)) && '-' == answer[3]) ;
149 #else
150         answer = xmalloc_getline(stdin);
151 #endif
152         if (answer) {
153                 int n = atoi(answer);
154                 alarm(0);
155                 if (ENABLE_FEATURE_CLEAN_UP) {
156                         free(msg);
157                         free(answer);
158                 }
159                 if (-1 == code || n == code) {
160                         return n;
161                 }
162         }
163         kill_helper();
164         bb_error_msg_and_die("%s failed", msg);
165 }
166
167 static int smtp_check(const char *fmt, int code)
168 {
169         return smtp_checkp(fmt, NULL, code);
170 }
171
172 static void pop3_checkr(const char *fmt, const char *param, char **ret)
173 {
174         char *msg = command(fmt, param);
175         char *answer = xmalloc_getline(stdin);
176         if (answer && '+' == answer[0]) {
177                 alarm(0);
178                 if (ret)
179                         *ret = answer;
180                 else
181                         free(answer);
182                 return;
183         }
184         kill_helper();
185         bb_error_msg_and_die("%s failed", msg);
186 }
187
188 static void pop3_check(const char *fmt, const char *param)
189 {
190         pop3_checkr(fmt, param, NULL);
191 }
192
193 // strip argument of bad chars
194 static char *sane(char *str)
195 {
196         char *s = str;
197         char *p = s;
198         while (*s) {
199                 if (isalnum(*s) || '_' == *s || '-' == *s || '.' == *s || '@' == *s) {
200                         *p++ = *s;
201                 }
202                 s++;
203         }
204         *p = '\0';
205         return str;
206 }
207
208 static void pop3_message(int fd)
209 {
210         char *answer;
211         // read stdin, copy to file fd
212         while ((answer = xmalloc_fgets_str(stdin, "\r\n"))) {
213                 char *s = answer;
214                 if ('.' == answer[0]) {
215                         if ('.' == answer[1])
216                                 s++;
217                         else if ('\r' == answer[1] && '\n' == answer[2] && '\0' == answer[3])
218                                 break;
219                 }
220                 xwrite(fd, s, strlen(s));
221                 free(answer);
222         }
223         close(fd);
224 }
225
226 static const char *args[] = {
227         "openssl", "s_client", "-quiet", "-connect", NULL, "-tls1", "-starttls", "smtp", NULL
228 };
229 #define opt_connect     args[4]
230 #define opt_after_connect args[5]
231
232 int sendgetmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
233 int sendgetmail_main(int argc, char **argv)
234 {
235         llist_t *recipients = NULL;
236         char *from;
237         const char *subject;
238         char *charset = (char *)"utf-8";
239
240         const char *opt_user;
241         const char *opt_pass;
242         const char *opt_timeout;
243         const char *opt_chdir;
244
245         enum {
246                 OPT_C = 1 << 0,         // chdir
247                 OPT_w = 1 << 1,         // network timeout
248                 OPT_U = 1 << 2,         // user
249                 OPT_P = 1 << 3,         // password
250                 OPT_X = 1 << 4,         // use openssl connection helper
251
252                 OPTS_t = 1 << 5,        // sendmail "to"
253                 OPTF_t = 1 << 5,        // fetchmail "TOP"
254
255                 OPTS_f = 1 << 6,        // sendmail "from"
256                 OPTF_z = 1 << 6,        // fetchmail "delete"
257
258                 OPTS_n = 1 << 7,        // notification
259                 OPTS_s = 1 << 8,        // subject given
260                 OPTS_c = 1 << 9,        // charset for subject and body
261         };
262
263         const char *options;
264         unsigned opts;
265
266         // SENDMAIL
267         if ('s' == applet_name[0]) {
268                 // save initial stdin
269                 xdup2(STDIN_FILENO, INITIAL_STDIN_FILENO);
270                 // -f must be specified
271                 // -t may be multiple
272                 opt_complementary = "-1:f:t::";
273                 options = "C:w:U:P:X" "t:f:ns:c:";
274         // FETCHMAIL
275         } else {
276                 opt_after_connect = NULL;
277                 opt_complementary = "=1:P";
278                 options = "C:w:U:P:X" "tz";
279         }
280         opts = getopt32(argv, options,
281                 &opt_chdir, &opt_timeout, &opt_user, &opt_pass,
282                 &recipients, &from, &subject, &charset
283         );
284
285         //argc -= optind;
286         argv += optind;
287
288         // first argument is remote server[:port]
289         opt_connect = *argv++;
290
291         if (opts & OPT_w)
292                 timeout = xatou(opt_timeout);
293
294         // chdir
295         if (opts & OPT_C)
296                 xchdir(opt_chdir);
297
298         // connect to server
299         if (opts & OPT_X) {
300                 launch_helper(args);
301         } else {
302                 // no connection helper provided -> make plain connect
303                 int fd = create_and_connect_stream_or_die(opt_connect, 0);
304                 xmove_fd(fd, STDIN_FILENO);
305                 xdup2(STDIN_FILENO, STDOUT_FILENO);
306         }
307
308         // randomize
309         srand(time(NULL));
310
311         // SENDMAIL
312         if (recipients) {
313                 int code;
314                 char *boundary;
315
316                 // wait for initial OK on plain connect
317                 if (!(opts & OPT_X))
318                         smtp_check(NULL, 220);
319
320                 sane(from);
321                 // introduce to server
322                 // should we respect modern (but useless here) EHLO?
323                 // or should they respect we wanna be tiny?!
324                 if (!ENABLE_FEATURE_SENDMAIL_EHLO || 250 != smtp_checkp("EHLO %s", from, -1)) {
325                         smtp_checkp("HELO %s", from, 250);
326                 }
327
328                 // set sender
329                 // NOTE: if password has not been specified ->
330                 // no authentication is possible
331                 code = (opts & OPT_P) ? -1 : 250;
332                 // first try softly without authentication
333                 while (250 != smtp_checkp("MAIL FROM:<%s>", from, code)) {
334                         // MAIL FROM failed -> authentication needed
335                         // do we have username?
336                         if (!(opts & OPT_U)) {
337                                 // no! fetch it from "from" option
338                                 //opts |= OPT_U;
339                                 opt_user = xstrdup(from);
340                                 *strchrnul(opt_user, '@') = '\0';
341                         }
342                         // now it seems we have username
343                         // try to authenticate
344                         if (334 == smtp_check("AUTH LOGIN", -1)) {
345                                 uuencode(NULL, opt_user);
346                                 smtp_check("", 334);
347                                 uuencode(NULL, opt_pass);
348                                 smtp_check("", 235);
349                         }
350                         // authenticated -> retry set sender
351                         // but now die on failure
352                         code = 250;
353                 }
354                 // set recipients
355                 for (llist_t *to = recipients; to; to = to->link) {
356                         smtp_checkp("RCPT TO:<%s>", sane(to->data), 250);
357                 }
358
359                 // now put message
360                 smtp_check("DATA", 354);
361                 // put address headers
362                 printf("From: %s\r\n", from);
363                 for (llist_t *to = recipients; to; to = to->link) {
364                         printf("To: %s\r\n", to->data);
365                 }
366                 // put encoded subject
367                 if (opts & OPTS_c)
368                         sane(charset);
369                 if (opts & OPTS_s) {
370                         printf("Subject: =?%s?B?", charset);
371                         uuencode(NULL, subject);
372                         printf("?=\r\n");
373                 }
374                 // put notification
375                 if (opts & OPTS_n)
376                         printf("Disposition-Notification-To: %s\r\n", from);
377                 // put common headers and body start
378                 boundary = xasprintf("%d-%d-%d", rand(), rand(), rand());
379                 printf(
380                         USE_FEATURE_SENDMAIL_BLOATY("X-Mailer: busybox " BB_VER " sendmail\r\n")
381                         "Message-ID: <%s>\r\n"
382                         "Mime-Version: 1.0\r\n"
383                         "%smultipart/mixed; boundary=\"%s\"\r\n"
384                         , boundary
385                         , "Content-Type: "
386                         , boundary
387                 );
388                 // put body + attachment(s)
389         {
390                 const char *fmt =
391                         "\r\n--%s\r\n"
392                         "%stext/plain; charset=%s\r\n"
393                         "%s%s\r\n"
394                         "%s"
395                 ;
396                 const char *p = charset;
397                 char *q = (char *)"";
398                 while (argv[0]) {
399                         printf(
400                                 fmt
401                                 , boundary
402                                 , "Content-Type: "
403                                 , p
404                                 , "Content-Disposition: inline"
405                                 , q
406                                 , "Content-Transfer-Encoding: base64\r\n"
407                         );
408                         p = "";
409                         fmt =
410                                 "\r\n--%s\r\n"
411                                 "%sapplication/octet-stream%s\r\n"
412                                 "%s; filename=\"%s\"\r\n"
413                                 "%s"
414                         ;
415                         uuencode(*argv, NULL);
416                         if (*(++argv))
417                                 q = bb_get_last_path_component_strip(argv[0]);
418                 }
419         }
420                 // put terminator
421                 printf("\r\n--%s--\r\n" "\r\n", boundary);
422                 if (ENABLE_FEATURE_CLEAN_UP)
423                         free(boundary);
424
425                 // end message and say goodbye
426                 smtp_check(".", 250);
427                 smtp_check("QUIT", 221);
428
429         // FETCHMAIL
430         } else {
431                 // authenticate
432                 char *buf;
433                 unsigned nmsg;
434                 if (!(opts & OPT_U)) {
435                         //opts |= OPT_U;
436                         opt_user = getenv("USER");
437                 }
438 #if ENABLE_FEATURE_FETCHMAIL_APOP
439                 pop3_checkr(NULL, NULL, &buf);
440                 // server supports APOP?
441                 if ('<' == buf[4]) {
442                         md5_ctx_t md5;
443                         uint8_t hex[16*2 + 1];
444                         // yes. compose <stamp><password>
445                         char *s = strchr(buf, '>');
446                         if (s)
447                                 strcpy(s+1, opt_pass);
448                         s = buf+4;
449                         // get md5 sum of <stamp><password>
450                         md5_begin(&md5);
451                         md5_hash(s, strlen(s), &md5);
452                         md5_end(s, &md5);
453                         bin2hex(hex, s, 16);
454                         // APOP
455                         s = xasprintf("%s %s", opt_user, hex);
456                         pop3_check("APOP %s", s);
457                         if (ENABLE_FEATURE_CLEAN_UP) {
458                                 free(s);
459                                 free(buf);
460                         }
461                 } else {
462 #else
463                 {
464                         pop3_check(NULL, NULL);
465 #endif
466                         // USER
467                         pop3_check("USER %s", opt_user);
468                         // PASS
469                         pop3_check("PASS %s", opt_pass);
470                 }
471
472                 // get statistics
473                 pop3_checkr("STAT", NULL, &buf);
474
475                 // get number of messages
476                 nmsg = atoi(buf+4);
477                 if (ENABLE_FEATURE_CLEAN_UP)
478                         free(buf);
479
480                 // lock maildir
481                 ////USE_FEATURE_CLEAN_UP(close)(xopen(".lock", O_CREAT | O_WRONLY | O_TRUNC | O_EXCL));
482                 
483                 // make tempnam(dir, salt) respect dir argument
484                 unsetenv("TMPDIR");
485
486                 // TODO: piping through external filter argv... if *argv
487
488                 // cache fetch command
489         {
490                 const char *retr = (opts & OPTF_t) ? "TOP %u 0" : "RETR %u";
491                 // loop through messages
492                 for (; nmsg; nmsg--) {
493                         int fd;
494                         char tmp_name[sizeof("tmp/XXXXXX")];
495                         char new_name[sizeof("new/XXXXXX")];
496
497                         // retrieve message in ./tmp
498                         strcpy(tmp_name, "tmp/XXXXXX");
499                         fd = mkstemp(tmp_name);
500                         if (fd < 0)
501                                 bb_perror_msg_and_die("cannot create unique file");
502                         pop3_check(retr, (const char *)nmsg);
503                         pop3_message(fd); // NB: closes fd
504
505                         // move file to ./new atomically
506                         strncpy(new_name, "new", 3);
507                         strcpy(new_name + 3, tmp_name + 3);
508                         if (rename(tmp_name, new_name) < 0) {
509                                 // rats! such file exists! try to make unique name
510                                 strcpy(new_name + 3, "tmp/XXXXXX" + 3);
511                                 fd = mkstemp(new_name);
512                                 if (fd < 0)
513                                         bb_perror_msg_and_die("cannot create unique file");
514                                 close(fd);
515                                 xrename(tmp_name, new_name);
516                         }
517
518                         // delete message from server
519                         if (opts & OPTF_z)
520                                 pop3_check("DELE %u", (const char*)nmsg);
521                 }
522         }
523
524                 // Bye
525                 pop3_check("QUIT", NULL);
526
527                 // unlock maildir
528                 ////unlink(".lock");
529         }
530
531         return 0;
532 }