/* vi: set sw=4 ts=4: */ /* * makemime: create MIME-encoded message * reformime: parse MIME-encoded message * * Copyright (C) 2008 by Vladimir Dronnikov * * Licensed under GPLv2, see file LICENSE in this source tree. */ #include "libbb.h" #include "mail.h" /* makemime -c type [-o file] [-e encoding] [-C charset] [-N name] \ [-a "Header: Contents"] file -m [ type ] [-o file] [-e encoding] [-a "Header: Contents"] file -j [-o file] file1 file2 @file file: filename - read or write from filename - - read or write from stdin or stdout &n - read or write from file descriptor n \( opts \) - read from child process, that generates [ opts ] Options: -c type - create a new MIME section from "file" with this Content-Type: (default is application/octet-stream). -C charset - MIME charset of a new text/plain section. -N name - MIME content name of the new mime section. -m [ type ] - create a multipart mime section from "file" of this Content-Type: (default is multipart/mixed). -e encoding - use the given encoding (7bit, 8bit, quoted-printable, or base64), instead of guessing. Omit "-e" and use -c auto to set Content-Type: to text/plain or application/octet-stream based on picked encoding. -j file1 file2 - join mime section file2 to multipart section file1. -o file - write the result to file, instead of stdout (not allowed in child processes). -a header - prepend an additional header to the output. @file - read all of the above options from file, one option or value on each line. {which version of makemime is this? What do we support?} */ /* In busybox 1.15.0.svn, makemime generates output like this * (empty lines are shown exactly!): {headers added with -a HDR} Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="24269534-2145583448-1655890676" --24269534-2145583448-1655890676 Content-Type: {set by -c, e.g. text/plain}; charset={set by -C, e.g. us-ascii} Content-Disposition: inline; filename="A" Content-Transfer-Encoding: base64 ...file A contents... --24269534-2145583448-1655890676 Content-Type: {set by -c, e.g. text/plain}; charset={set by -C, e.g. us-ascii} Content-Disposition: inline; filename="B" Content-Transfer-Encoding: base64 ...file B contents... --24269534-2145583448-1655890676-- */ /* For reference: here is an example email to LKML which has * 1st unnamed part (so it serves as an email body) * and one attached file: ...other headers... Content-Type: multipart/mixed; boundary="=-tOfTf3byOS0vZgxEWcX+" ...other headers... Mime-Version: 1.0 ...other headers... --=-tOfTf3byOS0vZgxEWcX+ Content-Type: text/plain Content-Transfer-Encoding: 7bit ...email text... ...email text... --=-tOfTf3byOS0vZgxEWcX+ Content-Disposition: attachment; filename="xyz" Content-Type: text/plain; name="xyz"; charset="UTF-8" Content-Transfer-Encoding: 7bit ...file contents... ...file contents... --=-tOfTf3byOS0vZgxEWcX+-- ...random junk added by mailing list robots and such... */ int makemime_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; int makemime_main(int argc UNUSED_PARAM, char **argv) { llist_t *opt_headers = NULL, *l; const char *opt_output; #define boundary opt_output enum { OPT_c = 1 << 0, // Content-Type: OPT_e = 1 << 1, // Content-Transfer-Encoding. Ignored. Assumed base64 OPT_o = 1 << 2, // output to OPT_C = 1 << 3, // charset OPT_N = 1 << 4, // COMPAT OPT_a = 1 << 5, // additional headers OPT_m = 1 << 6, // COMPAT OPT_j = 1 << 7, // COMPAT }; INIT_G(); // parse options opt_complementary = "a::"; opts = getopt32(argv, "c:e:o:C:N:a:m:j:", &G.content_type, NULL, &opt_output, &G.opt_charset, NULL, &opt_headers, NULL, NULL ); //argc -= optind; argv += optind; // respect -o output if (opts & OPT_o) freopen(opt_output, "w", stdout); // no files given on command line? -> use stdin if (!*argv) *--argv = (char *)"-"; // put additional headers for (l = opt_headers; l; l = l->link) puts(l->data); // make a random string -- it will delimit message parts srand(monotonic_us()); boundary = xasprintf("%u-%u-%u", (unsigned)rand(), (unsigned)rand(), (unsigned)rand()); // put multipart header printf( "Mime-Version: 1.0\n" "Content-Type: multipart/mixed; boundary=\"%s\"\n" , boundary ); // put attachments while (*argv) { printf( "\n--%s\n" "Content-Type: %s; charset=%s\n" "Content-Disposition: inline; filename=\"%s\"\n" "Content-Transfer-Encoding: base64\n" , boundary , G.content_type , G.opt_charset , bb_get_last_path_component_strip(*argv) ); encode_base64(*argv++, (const char *)stdin, ""); } // put multipart footer printf("\n--%s--\n" "\n", boundary); return EXIT_SUCCESS; #undef boundary } static const char *find_token(const char *const string_array[], const char *key, const char *defvalue) { const char *r = NULL; int i; for (i = 0; string_array[i] != NULL; i++) { if (strcasecmp(string_array[i], key) == 0) { r = (char *)string_array[i+1]; break; } } return (r) ? r : defvalue; } static const char *xfind_token(const char *const string_array[], const char *key) { const char *r = find_token(string_array, key, NULL); if (r) return r; bb_error_msg_and_die("header: %s", key); } enum { OPT_x = 1 << 0, OPT_X = 1 << 1, #if ENABLE_FEATURE_REFORMIME_COMPAT OPT_d = 1 << 2, OPT_e = 1 << 3, OPT_i = 1 << 4, OPT_s = 1 << 5, OPT_r = 1 << 6, OPT_c = 1 << 7, OPT_m = 1 << 8, OPT_h = 1 << 9, OPT_o = 1 << 10, OPT_O = 1 << 11, #endif }; static int parse(const char *boundary, char **argv) { char *line, *s, *p; const char *type; int boundary_len = strlen(boundary); const char *delims = " ;\"\t\r\n"; const char *uniq; int ntokens; const char *tokens[32]; // 32 is enough // prepare unique string pattern uniq = xasprintf("%%llu.%u.%s", (unsigned)getpid(), safe_gethostname()); //bb_info_msg("PARSE[%s]", uniq); while ((line = xmalloc_fgets_str(stdin, "\r\n\r\n")) != NULL) { // seek to start of MIME section // N.B. to avoid false positives let us seek to the _last_ occurance p = NULL; s = line; while ((s = strcasestr(s, "Content-Type:")) != NULL) p = s++; if (!p) goto next; //bb_info_msg("L[%s]", p); // split to tokens // TODO: strip of comments which are of form: (comment-text) ntokens = 0; tokens[ntokens] = NULL; for (s = strtok(p, delims); s; s = strtok(NULL, delims)) { tokens[ntokens] = s; if (ntokens < ARRAY_SIZE(tokens) - 1) ntokens++; //bb_info_msg("L[%d][%s]", ntokens, s); } tokens[ntokens] = NULL; //bb_info_msg("N[%d]", ntokens); // analyse tokens type = find_token(tokens, "Content-Type:", "text/plain"); //bb_info_msg("T[%s]", type); if (0 == strncasecmp(type, "multipart/", 10)) { if (0 == strcasecmp(type+10, "mixed")) { parse(xfind_token(tokens, "boundary="), argv); } else bb_error_msg_and_die("no support of content type '%s'", type); } else { pid_t pid = pid; int rc; FILE *fp; // fetch charset const char *charset = find_token(tokens, "charset=", CONFIG_FEATURE_MIME_CHARSET); // fetch encoding const char *encoding = find_token(tokens, "Content-Transfer-Encoding:", "7bit"); // compose target filename char *filename = (char *)find_token(tokens, "filename=", NULL); if (!filename) filename = xasprintf(uniq, monotonic_us()); else filename = bb_get_last_path_component_strip(xstrdup(filename)); // start external helper, if any if (opts & OPT_X) { int fd[2]; xpipe(fd); pid = vfork(); if (0 == pid) { // child reads from fd[0] close(fd[1]); xmove_fd(fd[0], STDIN_FILENO); xsetenv("CONTENT_TYPE", type); xsetenv("CHARSET", charset); xsetenv("ENCODING", encoding); xsetenv("FILENAME", filename); BB_EXECVP_or_die(argv); } // parent dumps to fd[1] close(fd[0]); fp = xfdopen_for_write(fd[1]); signal(SIGPIPE, SIG_IGN); // ignore EPIPE // or create a file for dump } else { char *fname = xasprintf("%s%s", *argv, filename); fp = xfopen_for_write(fname); free(fname); } // housekeeping free(filename); // dump to fp if (0 == strcasecmp(encoding, "base64")) { read_base64(stdin, fp, '-'); } else if (0 != strcasecmp(encoding, "7bit") && 0 != strcasecmp(encoding, "8bit") ) { // quoted-printable, binary, user-defined are unsupported so far bb_error_msg_and_die("no support of encoding '%s'", encoding); } else { // N.B. we have written redundant \n. so truncate the file // The following weird 2-tacts reading technique is due to // we have to not write extra \n at the end of the file // In case of -x option we could truncate the resulting file as // fseek(fp, -1, SEEK_END); // if (ftruncate(fileno(fp), ftell(fp))) // bb_perror_msg("ftruncate"); // But in case of -X we have to be much more careful. There is // no means to truncate what we already have sent to the helper. p = xmalloc_fgets_str(stdin, "\r\n"); while (p) { s = xmalloc_fgets_str(stdin, "\r\n"); if (s == NULL) break; if ('-' == s[0] && '-' == s[1] && 0 == strncmp(s+2, boundary, boundary_len) ) { break; } fputs(p, fp); p = s; } /* while ((s = xmalloc_fgetline_str(stdin, "\r\n")) != NULL) { if ('-' == s[0] && '-' == s[1] && 0 == strncmp(s+2, boundary, boundary_len)) break; fprintf(fp, "%s\n", s); } // N.B. we have written redundant \n. so truncate the file fseek(fp, -1, SEEK_END); if (ftruncate(fileno(fp), ftell(fp))) bb_perror_msg("ftruncate"); */ } fclose(fp); // finalize helper if (opts & OPT_X) { signal(SIGPIPE, SIG_DFL); // exit if helper exited >0 rc = (wait4pid(pid) & 0xff); if (rc) return rc+20; } // check multipart finalized if (s && '-' == s[2+boundary_len] && '-' == s[2+boundary_len+1]) { free(line); break; } } next: free(line); } //bb_info_msg("ENDPARSE[%s]", boundary); return EXIT_SUCCESS; } /* Usage: reformime [options] -d - parse a delivery status notification. -e - extract contents of MIME section. -x - extract MIME section to a file. -X - pipe MIME section to a program. -i - show MIME info. -s n.n.n.n - specify MIME section. -r - rewrite message, filling in missing MIME headers. -r7 - also convert 8bit/raw encoding to quoted-printable, if possible. -r8 - also convert quoted-printable encoding to 8bit, if possible. -c charset - default charset for rewriting, -o, and -O. -m [file] [file]... - create a MIME message digest. -h "header" - decode RFC 2047-encoded header. -o "header" - encode unstructured header using RFC 2047. -O "header" - encode address list header using RFC 2047. */ int reformime_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; int reformime_main(int argc UNUSED_PARAM, char **argv) { const char *opt_prefix = ""; INIT_G(); // parse options // N.B. only -x and -X are supported so far opt_complementary = "x--X:X--x" IF_FEATURE_REFORMIME_COMPAT(":m::"); opts = getopt32(argv, "x:X" IF_FEATURE_REFORMIME_COMPAT("deis:r:c:m:h:o:O:"), &opt_prefix IF_FEATURE_REFORMIME_COMPAT(, NULL, NULL, &G.opt_charset, NULL, NULL, NULL, NULL) ); //argc -= optind; argv += optind; return parse("", (opts & OPT_X) ? argv : (char **)&opt_prefix); }