2f99df6c06fb04ee95c695d11fe85fa64bbe6c54
[platform/upstream/busybox.git] / mailutils / sendmail.c
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 tarball for details.
8  */
9 #include "libbb.h"
10 #include "mail.h"
11
12 // limit maximum allowed number of headers to prevent overflows.
13 // set to 0 to not limit
14 #define MAX_HEADERS 256
15
16 static int smtp_checkp(const char *fmt, const char *param, int code)
17 {
18         char *answer;
19         const char *msg = command(fmt, param);
20         // read stdin
21         // if the string has a form \d\d\d- -- read next string. E.g. EHLO response
22         // parse first bytes to a number
23         // if code = -1 then just return this number
24         // if code != -1 then checks whether the number equals the code
25         // if not equal -> die saying msg
26         while ((answer = xmalloc_fgetline(stdin)) != NULL)
27                 if (strlen(answer) <= 3 || '-' != answer[3])
28                         break;
29         if (answer) {
30                 int n = atoi(answer);
31                 if (timeout)
32                         alarm(0);
33                 free(answer);
34                 if (-1 == code || n == code)
35                         return n;
36         }
37         bb_error_msg_and_die("%s failed", msg);
38 }
39
40 static int smtp_check(const char *fmt, int code)
41 {
42         return smtp_checkp(fmt, NULL, code);
43 }
44
45 // strip argument of bad chars
46 static char *sane_address(char *str)
47 {
48         char *s = str;
49         char *p = s;
50         while (*s) {
51                 if (isalnum(*s) || '_' == *s || '-' == *s || '.' == *s || '@' == *s) {
52                         *p++ = *s;
53                 }
54                 s++;
55         }
56         *p = '\0';
57         return str;
58 }
59
60 static void rcptto(const char *s)
61 {
62         // N.B. we don't die if recipient is rejected, for the other recipients may be accepted
63         if (250 != smtp_checkp("RCPT TO:<%s>", s, -1))
64                 bb_error_msg("Bad recipient: <%s>", s);
65 }
66
67 int sendmail_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
68 int sendmail_main(int argc UNUSED_PARAM, char **argv)
69 {
70         char *opt_connect = opt_connect;
71         char *opt_from;
72         char *s;
73         llist_t *list = NULL;
74         char *domain = sane_address(safe_getdomainname());
75         unsigned nheaders = 0;
76         int code;
77
78         enum {
79         //--- standard options
80                 OPT_t = 1 << 0,         // read message for recipients, append them to those on cmdline
81                 OPT_f = 1 << 1,         // sender address
82                 OPT_o = 1 << 2,         // various options. -oi IMPLIED! others are IGNORED!
83                 OPT_i = 1 << 3,         // IMPLIED!
84         //--- BB specific options
85                 OPT_w = 1 << 4,         // network timeout
86                 OPT_H = 1 << 5,         // use external connection helper
87                 OPT_S = 1 << 6,         // specify connection string
88                 OPT_a = 1 << 7,         // authentication tokens
89         };
90
91         // init global variables
92         INIT_G();
93
94         // save initial stdin since body is piped!
95         xdup2(STDIN_FILENO, 3);
96         G.fp0 = fdopen(3, "r");
97
98         // parse options
99         // -f is required. -H and -S are mutually exclusive
100         opt_complementary = "f:w+:H--S:S--H:a::";
101         // N.B. since -H and -S are mutually exclusive they do not interfere in opt_connect
102         // -a is for ssmtp (http://downloads.openwrt.org/people/nico/man/man8/ssmtp.8.html) compatibility,
103         // it is still under development.
104         opts = getopt32(argv, "tf:o:iw:H:S:a::", &opt_from, NULL, &timeout, &opt_connect, &opt_connect, &list);
105         //argc -= optind;
106         argv += optind;
107
108         // process -a[upm]<token> options
109         if ((opts & OPT_a) && !list)
110                 bb_show_usage();
111         while (list) {
112                 char *a = (char *) llist_pop(&list);
113                 if ('u' == a[0])
114                         G.user = xstrdup(a+1);
115                 if ('p' == a[0])
116                         G.pass = xstrdup(a+1);
117                 // N.B. we support only AUTH LOGIN so far
118                 //if ('m' == a[0])
119                 //      G.method = xstrdup(a+1);
120         }
121         // N.B. list == NULL here
122         //bb_info_msg("OPT[%x] AU[%s], AP[%s], AM[%s], ARGV[%s]", opts, au, ap, am, *argv);
123
124         // connect to server
125
126         // connection helper ordered? ->
127         if (opts & OPT_H) {
128                 const char *args[] = { "sh", "-c", opt_connect, NULL };
129                 // plug it in
130                 launch_helper(args);
131         // vanilla connection
132         } else {
133                 int fd;
134                 // host[:port] not explicitly specified? -> use $SMTPHOST
135                 // no $SMTPHOST ? -> use localhost
136                 if (!(opts & OPT_S)) {
137                         opt_connect = getenv("SMTPHOST");
138                         if (!opt_connect)
139                                 opt_connect = (char *)"127.0.0.1";
140                 }
141                 // do connect
142                 fd = create_and_connect_stream_or_die(opt_connect, 25);
143                 // and make ourselves a simple IO filter
144                 xmove_fd(fd, STDIN_FILENO);
145                 xdup2(STDIN_FILENO, STDOUT_FILENO);
146         }
147         // N.B. from now we know nothing about network :)
148
149         // wait for initial server OK
150         // N.B. if we used openssl the initial 220 answer is already swallowed during openssl TLS init procedure
151         // so we need to kick the server to see whether we are ok
152         code = smtp_check("NOOP", -1);
153         // 220 on plain connection, 250 on openssl-helped TLS session
154         if (220 == code)
155                 smtp_check(NULL, 250); // reread the code to stay in sync
156         else if (250 != code)
157                 bb_error_msg_and_die("INIT failed");
158
159         // we should start with modern EHLO
160         if (250 != smtp_checkp("EHLO %s", domain, -1)) {
161                 smtp_checkp("HELO %s", domain, 250);
162         }
163         if (ENABLE_FEATURE_CLEAN_UP)
164                 free(domain);
165
166         // perform authentication
167         if (opts & OPT_a) {
168                 smtp_check("AUTH LOGIN", 334);
169                 // we must read credentials unless they are given via -a[up] options
170                 if (!G.user || !G.pass)
171                         get_cred_or_die(4);
172                 encode_base64(NULL, G.user, NULL);
173                 smtp_check("", 334);
174                 encode_base64(NULL, G.pass, NULL);
175                 smtp_check("", 235);
176         }
177
178         // set sender
179         // N.B. we have here a very loosely defined algotythm
180         // since sendmail historically offers no means to specify secrets on cmdline.
181         // 1) server can require no authentication ->
182         //      we must just provide a (possibly fake) reply address.
183         // 2) server can require AUTH ->
184         //      we must provide valid username and password along with a (possibly fake) reply address.
185         //      For the sake of security username and password are to be read either from console or from a secured file.
186         //      Since reading from console may defeat usability, the solution is either to read from a predefined
187         //      file descriptor (e.g. 4), or again from a secured file.
188
189         // got no sender address? -> use system username as a resort
190         // N.B. we marked -f as required option!
191         //if (!G.user) {
192         //      // N.B. IMHO getenv("USER") can be way easily spoofed!
193         //      G.user = xuid2uname(getuid());
194         //      opt_from = xasprintf("%s@%s", G.user, domain);
195         //}
196         //if (ENABLE_FEATURE_CLEAN_UP)
197         //      free(domain);
198         smtp_checkp("MAIL FROM:<%s>", opt_from, 250);
199
200         // process message
201
202         // read recipients from message and add them to those given on cmdline.
203         // this means we scan stdin for To:, Cc:, Bcc: lines until an empty line
204         // and then use the rest of stdin as message body
205         code = 0; // set "analyze headers" mode
206         while ((s = xmalloc_fgetline(G.fp0)) != NULL) {
207  dump:
208                 // put message lines doubling leading dots
209                 if (code) {
210                         // escape leading dots
211                         // N.B. this feature is implied even if no -i (-oi) switch given
212                         // N.B. we need to escape the leading dot regardless of
213                         // whether it is single or not character on the line
214                         if ('.' == s[0] /*&& '\0' == s[1] */)
215                                 printf(".");
216                         // dump read line
217                         printf("%s\r\n", s);
218                         free(s);
219                         continue;
220                 }
221
222                 // analyze headers
223                 // To: or Cc: headers add recipients
224                 if (0 == strncasecmp("To: ", s, 4) || 0 == strncasecmp("Bcc: " + 1, s, 4)) {
225                         rcptto(sane_address(s+4));
226                         goto addheader;
227                 // Bcc: header adds blind copy (hidden) recipient
228                 } else if (0 == strncasecmp("Bcc: ", s, 5)) {
229                         rcptto(sane_address(s+5));
230                         free(s);
231                         // N.B. Bcc: vanishes from headers!
232
233                 // other headers go verbatim
234
235                 // N.B. RFC2822 2.2.3 "Long Header Fields" allows for headers to occupy several lines.
236                 // Continuation is denoted by prefixing additional lines with whitespace(s).
237                 // Thanks (stefan.seyfried at googlemail.com) for pointing this out.
238                 } else if (strchr(s, ':') || (list && skip_whitespace(s) != s)) {
239  addheader:
240                         // N.B. we allow MAX_HEADERS generic headers at most to prevent attacks
241                         if (MAX_HEADERS && ++nheaders >= MAX_HEADERS)
242                                 goto bail;
243                         llist_add_to_end(&list, s);
244                 // a line without ":" (an empty line too, by definition) doesn't look like a valid header
245                 // so stop "analyze headers" mode
246                 } else {
247  reenter:
248                         // put recipients specified on cmdline
249                         while (*argv) {
250                                 char *t = sane_address(*argv);
251                                 rcptto(t);
252                                 //if (MAX_HEADERS && ++nheaders >= MAX_HEADERS)
253                                 //      goto bail;
254                                 llist_add_to_end(&list, xasprintf("To: %s", t));
255                                 argv++;
256                         }
257                         // enter "put message" mode
258                         // N.B. DATA fails iff no recipients were accepted (or even provided)
259                         // in this case just bail out gracefully
260                         if (354 != smtp_check("DATA", -1))
261                                 goto bail;
262                         // dump the headers
263                         while (list) {
264                                 printf("%s\r\n", (char *) llist_pop(&list));
265                         }
266                         // stop analyzing headers
267                         code++;
268                         // N.B. !s means: we read nothing, and nothing to be read in the future.
269                         // just dump empty line and break the loop
270                         if (!s) {
271                                 puts("\r");
272                                 break;
273                         }
274                         // go dump message body
275                         // N.B. "s" already contains the first non-header line, so pretend we read it from input
276                         goto dump;
277                 }
278         }
279         // odd case: we didn't stop "analyze headers" mode -> message body is empty. Reenter the loop
280         // N.B. after reenter code will be > 0
281         if (!code)
282                 goto reenter;
283
284         // finalize the message
285         smtp_check(".", 250);
286  bail:
287         // ... and say goodbye
288         smtp_check("QUIT", 221);
289         // cleanup
290         if (ENABLE_FEATURE_CLEAN_UP)
291                 fclose(G.fp0);
292
293         return EXIT_SUCCESS;
294 }