1/* vi: set sw=4 ts=4: */ 2/* 3 * makemime: create MIME-encoded message 4 * reformime: parse MIME-encoded message 5 * 6 * Copyright (C) 2008 by Vladimir Dronnikov <dronnikov@gmail.com> 7 * 8 * Licensed under GPLv2, see file LICENSE in this tarball for details. 9 */ 10#include "libbb.h" 11#include "mail.h" 12 13/* 14 makemime -c type [-o file] [-e encoding] [-C charset] [-N name] \ 15 [-a "Header: Contents"] file 16 -m [ type ] [-o file] [-e encoding] [-a "Header: Contents"] file 17 -j [-o file] file1 file2 18 @file 19 20 file: filename - read or write from filename 21 - - read or write from stdin or stdout 22 &n - read or write from file descriptor n 23 \( opts \) - read from child process, that generates [ opts ] 24 25Options: 26 27 -c type - create a new MIME section from "file" with this 28 Content-Type: (default is application/octet-stream). 29 -C charset - MIME charset of a new text/plain section. 30 -N name - MIME content name of the new mime section. 31 -m [ type ] - create a multipart mime section from "file" of this 32 Content-Type: (default is multipart/mixed). 33 -e encoding - use the given encoding (7bit, 8bit, quoted-printable, 34 or base64), instead of guessing. Omit "-e" and use 35 -c auto to set Content-Type: to text/plain or 36 application/octet-stream based on picked encoding. 37 -j file1 file2 - join mime section file2 to multipart section file1. 38 -o file - write the result to file, instead of stdout (not 39 allowed in child processes). 40 -a header - prepend an additional header to the output. 41 42 @file - read all of the above options from file, one option or 43 value on each line. 44 {which version of makemime is this? What do we support?} 45*/ 46 47 48/* In busybox 1.15.0.svn, makemime generates output like this 49 * (empty lines are shown exactly!): 50{headers added with -a HDR} 51Mime-Version: 1.0 52Content-Type: multipart/mixed; boundary="24269534-2145583448-1655890676" 53 54--24269534-2145583448-1655890676 55Content-Type: {set by -c, e.g. text/plain}; charset={set by -C, e.g. us-ascii} 56Content-Disposition: inline; filename="A" 57Content-Transfer-Encoding: base64 58 59...file A contents... 60--24269534-2145583448-1655890676 61Content-Type: {set by -c, e.g. text/plain}; charset={set by -C, e.g. us-ascii} 62Content-Disposition: inline; filename="B" 63Content-Transfer-Encoding: base64 64 65...file B contents... 66--24269534-2145583448-1655890676-- 67 68*/ 69 70 71/* For reference: here is an example email to LKML which has 72 * 1st unnamed part (so it serves as an email body) 73 * and one attached file: 74...other headers... 75Content-Type: multipart/mixed; boundary="=-tOfTf3byOS0vZgxEWcX+" 76...other headers... 77Mime-Version: 1.0 78...other headers... 79 80 81--=-tOfTf3byOS0vZgxEWcX+ 82Content-Type: text/plain 83Content-Transfer-Encoding: 7bit 84 85...email text... 86...email text... 87 88 89--=-tOfTf3byOS0vZgxEWcX+ 90Content-Disposition: attachment; filename="xyz" 91Content-Type: text/plain; name="xyz"; charset="UTF-8" 92Content-Transfer-Encoding: 7bit 93 94...file contents... 95...file contents... 96 97--=-tOfTf3byOS0vZgxEWcX+-- 98 99...random junk added by mailing list robots and such... 100*/ 101 102int makemime_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; 103int makemime_main(int argc UNUSED_PARAM, char **argv) 104{ 105 llist_t *opt_headers = NULL, *l; 106 const char *opt_output; 107#define boundary opt_output 108 109 enum { 110 OPT_c = 1 << 0, // Content-Type: 111 OPT_e = 1 << 1, // Content-Transfer-Encoding. Ignored. Assumed base64 112 OPT_o = 1 << 2, // output to 113 OPT_C = 1 << 3, // charset 114 OPT_N = 1 << 4, // COMPAT 115 OPT_a = 1 << 5, // additional headers 116 OPT_m = 1 << 6, // COMPAT 117 OPT_j = 1 << 7, // COMPAT 118 }; 119 120 INIT_G(); 121 122 // parse options 123 opt_complementary = "a::"; 124 opts = getopt32(argv, 125 "c:e:o:C:N:a:m:j:", 126 &G.content_type, NULL, &opt_output, &G.opt_charset, NULL, &opt_headers, NULL, NULL 127 ); 128 //argc -= optind; 129 argv += optind; 130 131 // respect -o output 132 if (opts & OPT_o) 133 freopen(opt_output, "w", stdout); 134 135 // no files given on command line? -> use stdin 136 if (!*argv) 137 *--argv = (char *)"-"; 138 139 // put additional headers 140 for (l = opt_headers; l; l = l->link) 141 puts(l->data); 142 143 // make a random string -- it will delimit message parts 144 srand(monotonic_us()); 145 boundary = xasprintf("%u-%u-%u", 146 (unsigned)rand(), (unsigned)rand(), (unsigned)rand()); 147 148 // put multipart header 149 printf( 150 "Mime-Version: 1.0\n" 151 "Content-Type: multipart/mixed; boundary=\"%s\"\n" 152 , boundary 153 ); 154 155 // put attachments 156 while (*argv) { 157 printf( 158 "\n--%s\n" 159 "Content-Type: %s; charset=%s\n" 160 "Content-Disposition: inline; filename=\"%s\"\n" 161 "Content-Transfer-Encoding: base64\n" 162 , boundary 163 , G.content_type 164 , G.opt_charset 165 , bb_get_last_path_component_strip(*argv) 166 ); 167 encode_base64(*argv++, (const char *)stdin, ""); 168 } 169 170 // put multipart footer 171 printf("\n--%s--\n" "\n", boundary); 172 173 return EXIT_SUCCESS; 174#undef boundary 175} 176 177static const char *find_token(const char *const string_array[], const char *key, const char *defvalue) 178{ 179 const char *r = NULL; 180 int i; 181 for (i = 0; string_array[i] != NULL; i++) { 182 if (strcasecmp(string_array[i], key) == 0) { 183 r = (char *)string_array[i+1]; 184 break; 185 } 186 } 187 return (r) ? r : defvalue; 188} 189 190static const char *xfind_token(const char *const string_array[], const char *key) 191{ 192 const char *r = find_token(string_array, key, NULL); 193 if (r) 194 return r; 195 bb_error_msg_and_die("header: %s", key); 196} 197 198enum { 199 OPT_x = 1 << 0, 200 OPT_X = 1 << 1, 201#if ENABLE_FEATURE_REFORMIME_COMPAT 202 OPT_d = 1 << 2, 203 OPT_e = 1 << 3, 204 OPT_i = 1 << 4, 205 OPT_s = 1 << 5, 206 OPT_r = 1 << 6, 207 OPT_c = 1 << 7, 208 OPT_m = 1 << 8, 209 OPT_h = 1 << 9, 210 OPT_o = 1 << 10, 211 OPT_O = 1 << 11, 212#endif 213}; 214 215static int parse(const char *boundary, char **argv) 216{ 217 char *line, *s, *p; 218 const char *type; 219 int boundary_len = strlen(boundary); 220 const char *delims = " ;\"\t\r\n"; 221 const char *uniq; 222 int ntokens; 223 const char *tokens[32]; // 32 is enough 224 225 // prepare unique string pattern 226 uniq = xasprintf("%%llu.%u.%s", (unsigned)getpid(), safe_gethostname()); 227 228//bb_info_msg("PARSE[%s]", terminator); 229 230 while ((line = xmalloc_fgets_str(stdin, "\r\n\r\n")) != NULL) { 231 232 // seek to start of MIME section 233 // N.B. to avoid false positives let us seek to the _last_ occurance 234 p = NULL; 235 s = line; 236 while ((s = strcasestr(s, "Content-Type:")) != NULL) 237 p = s++; 238 if (!p) 239 goto next; 240//bb_info_msg("L[%s]", p); 241 242 // split to tokens 243 // TODO: strip of comments which are of form: (comment-text) 244 ntokens = 0; 245 tokens[ntokens] = NULL; 246 for (s = strtok(p, delims); s; s = strtok(NULL, delims)) { 247 tokens[ntokens] = s; 248 if (ntokens < ARRAY_SIZE(tokens) - 1) 249 ntokens++; 250//bb_info_msg("L[%d][%s]", ntokens, s); 251 } 252 tokens[ntokens] = NULL; 253//bb_info_msg("N[%d]", ntokens); 254 255 // analyse tokens 256 type = find_token(tokens, "Content-Type:", "text/plain"); 257//bb_info_msg("T[%s]", type); 258 if (0 == strncasecmp(type, "multipart/", 10)) { 259 if (0 == strcasecmp(type+10, "mixed")) { 260 parse(xfind_token(tokens, "boundary="), argv); 261 } else 262 bb_error_msg_and_die("no support of content type '%s'", type); 263 } else { 264 pid_t pid = pid; 265 int rc; 266 FILE *fp; 267 // fetch charset 268 const char *charset = find_token(tokens, "charset=", CONFIG_FEATURE_MIME_CHARSET); 269 // fetch encoding 270 const char *encoding = find_token(tokens, "Content-Transfer-Encoding:", "7bit"); 271 // compose target filename 272 char *filename = (char *)find_token(tokens, "filename=", NULL); 273 if (!filename) 274 filename = xasprintf(uniq, monotonic_us()); 275 else 276 filename = bb_get_last_path_component_strip(xstrdup(filename)); 277 278 // start external helper, if any 279 if (opts & OPT_X) { 280 int fd[2]; 281 xpipe(fd); 282 pid = vfork(); 283 if (0 == pid) { 284 // child reads from fd[0] 285 close(fd[1]); 286 xmove_fd(fd[0], STDIN_FILENO); 287 xsetenv("CONTENT_TYPE", type); 288 xsetenv("CHARSET", charset); 289 xsetenv("ENCODING", encoding); 290 xsetenv("FILENAME", filename); 291 BB_EXECVP_or_die(argv); 292 } 293 // parent dumps to fd[1] 294 close(fd[0]); 295 fp = xfdopen_for_write(fd[1]); 296 signal(SIGPIPE, SIG_IGN); // ignore EPIPE 297 // or create a file for dump 298 } else { 299 char *fname = xasprintf("%s%s", *argv, filename); 300 fp = xfopen_for_write(fname); 301 free(fname); 302 } 303 304 // housekeeping 305 free(filename); 306 307 // dump to fp 308 if (0 == strcasecmp(encoding, "base64")) { 309 decode_base64(stdin, fp); 310 } else if (0 != strcasecmp(encoding, "7bit") 311 && 0 != strcasecmp(encoding, "8bit") 312 ) { 313 // quoted-printable, binary, user-defined are unsupported so far 314 bb_error_msg_and_die("no support of encoding '%s'", encoding); 315 } else { 316 // N.B. we have written redundant \n. so truncate the file 317 // The following weird 2-tacts reading technique is due to 318 // we have to not write extra \n at the end of the file 319 // In case of -x option we could truncate the resulting file as 320 // fseek(fp, -1, SEEK_END); 321 // if (ftruncate(fileno(fp), ftell(fp))) 322 // bb_perror_msg("ftruncate"); 323 // But in case of -X we have to be much more careful. There is 324 // no means to truncate what we already have sent to the helper. 325 p = xmalloc_fgets_str(stdin, "\r\n"); 326 while (p) { 327 s = xmalloc_fgets_str(stdin, "\r\n"); 328 if (s == NULL) 329 break; 330 if ('-' == s[0] 331 && '-' == s[1] 332 && 0 == strncmp(s+2, boundary, boundary_len) 333 ) { 334 break; 335 } 336 fputs(p, fp); 337 p = s; 338 } 339 340/* 341 while ((s = xmalloc_fgetline_str(stdin, "\r\n")) != NULL) { 342 if ('-' == s[0] && '-' == s[1] 343 && 0 == strncmp(s+2, boundary, boundary_len)) 344 break; 345 fprintf(fp, "%s\n", s); 346 } 347 // N.B. we have written redundant \n. so truncate the file 348 fseek(fp, -1, SEEK_END); 349 if (ftruncate(fileno(fp), ftell(fp))) 350 bb_perror_msg("ftruncate"); 351*/ 352 } 353 fclose(fp); 354 355 // finalize helper 356 if (opts & OPT_X) { 357 signal(SIGPIPE, SIG_DFL); 358 // exit if helper exited >0 359 rc = (wait4pid(pid) & 0xff); 360 if (rc) 361 return rc+20; 362 } 363 364 // check multipart finalized 365 if (s && '-' == s[2+boundary_len] && '-' == s[2+boundary_len+1]) { 366 free(line); 367 break; 368 } 369 } 370 next: 371 free(line); 372 } 373 374//bb_info_msg("ENDPARSE[%s]", boundary); 375 376 return EXIT_SUCCESS; 377} 378 379/* 380Usage: reformime [options] 381 -d - parse a delivery status notification. 382 -e - extract contents of MIME section. 383 -x - extract MIME section to a file. 384 -X - pipe MIME section to a program. 385 -i - show MIME info. 386 -s n.n.n.n - specify MIME section. 387 -r - rewrite message, filling in missing MIME headers. 388 -r7 - also convert 8bit/raw encoding to quoted-printable, if possible. 389 -r8 - also convert quoted-printable encoding to 8bit, if possible. 390 -c charset - default charset for rewriting, -o, and -O. 391 -m [file] [file]... - create a MIME message digest. 392 -h "header" - decode RFC 2047-encoded header. 393 -o "header" - encode unstructured header using RFC 2047. 394 -O "header" - encode address list header using RFC 2047. 395*/ 396 397int reformime_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; 398int reformime_main(int argc UNUSED_PARAM, char **argv) 399{ 400 const char *opt_prefix = ""; 401 402 INIT_G(); 403 404 // parse options 405 // N.B. only -x and -X are supported so far 406 opt_complementary = "x--X:X--x" IF_FEATURE_REFORMIME_COMPAT(":m::"); 407 opts = getopt32(argv, 408 "x:X" IF_FEATURE_REFORMIME_COMPAT("deis:r:c:m:h:o:O:"), 409 &opt_prefix 410 IF_FEATURE_REFORMIME_COMPAT(, NULL, NULL, &G.opt_charset, NULL, NULL, NULL, NULL) 411 ); 412 //argc -= optind; 413 argv += optind; 414 415 return parse("", (opts & OPT_X) ? argv : (char **)&opt_prefix); 416} 417