busybox/mailutils/mime.c
<<
>>
Prefs
   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