]> diplodocus.org Git - nmh/blobdiff - uip/mhfixmsg.c
Fixed inc(1) and %(me) to not obey Local-Mailbox profile component.
[nmh] / uip / mhfixmsg.c
index d5f9ab0d29b90c6241e72e10d6a3ea2b9b4c3900..f410ecb4ff8becf8d767d338bbb6aa8b24e29d24 100644 (file)
@@ -6,6 +6,27 @@
  */
 
 #include "h/mh.h"
+#include "sbr/m_name.h"
+#include "sbr/m_gmprot.h"
+#include "sbr/m_getfld.h"
+#include "sbr/getarguments.h"
+#include "sbr/concat.h"
+#include "sbr/seq_setprev.h"
+#include "sbr/seq_setcur.h"
+#include "sbr/seq_save.h"
+#include "sbr/smatch.h"
+#include "sbr/fmt_rfc2047.h"
+#include "sbr/cpydata.h"
+#include "sbr/trimcpy.h"
+#include "sbr/m_convert.h"
+#include "sbr/m_backup.h"
+#include "sbr/getfolder.h"
+#include "sbr/folder_read.h"
+#include "sbr/context_save.h"
+#include "sbr/context_replace.h"
+#include "sbr/context_find.h"
+#include "sbr/readconfig.h"
+#include "sbr/ambigsw.h"
 #include "sbr/path.h"
 #include "sbr/print_version.h"
 #include "sbr/print_help.h"
@@ -29,6 +50,8 @@
     X("decodetext 8bit|7bit|binary", 0, DECODETEXTSW) \
     X("nodecodetext", 0, NDECODETEXTSW) \
     X("decodetypes", 0, DECODETYPESW) \
+    X("decodeheaderfieldbodies utf-8", 0, DECODEHEADERFIELDBODIESSW) \
+    X("nodecodeheaderfieldbodies", 0, NDECODEHEADERFIELDBODIESSW) \
     X("crlflinebreaks", 0, CRLFLINEBREAKSSW) \
     X("nocrlflinebreaks", 0, NCRLFLINEBREAKSSW) \
     X("textcharset", 0, TEXTCHARSETSW) \
@@ -41,6 +64,8 @@
     X("nofixboundary", 0, NFIXBOUNDARYSW) \
     X("fixcte", 0, FIXCOMPOSITECTESW) \
     X("nofixcte", 0, NFIXCOMPOSITECTESW) \
+    X("checkbase64", 0, CHECKBASE64SW) \
+    X("nocheckbase64", 0, NCHECKBASE64SW) \
     X("fixtype mimetype", 0, FIXTYPESW) \
     X("file file", 0, FILESW) \
     X("outfile file", 0, OUTFILESW) \
@@ -70,7 +95,7 @@ int debugsw; /* Needed by mhparse.c. */
 /*
  * static prototypes
  */
-typedef struct fix_transformations {
+typedef struct {
     int fixboundary;
     int fixcompositecte;
     svector_t fixtypes;
@@ -78,9 +103,11 @@ typedef struct fix_transformations {
     int replacetextplain;
     int decodetext;
     char *decodetypes;
+    char *decodeheaderfieldbodies; /* Either NULL or "utf-8". */
     /* Whether to use CRLF linebreaks, per RFC 2046 Sec. 4.1.1, par.1. */
     int lf_line_endings;
     char *textcharset;
+    bool checkbase64;
 } fix_transformations;
 
 static int mhfixmsgsbr (CT *, char *, const fix_transformations *,
@@ -99,11 +126,15 @@ static int find_textplain_sibling (CT, int, int *);
 static int insert_new_text_plain_part (CT, int, CT);
 static CT build_text_plain_part (CT);
 static int insert_into_new_mp_alt (CT *, int *);
+static int insert_into_new_mp_mixed (CT *, const char *, int *);
 static CT divide_part (CT);
 static void copy_ctinfo (CI, CI);
 static int decode_part (CT);
+static size_t get_valid_base64 (CT, char **);
+static size_t find_invalid_base64_pos (const char *);
+static int check_base64_encoding (CT *);
 static int reformat_part (CT, char *, char *, char *, int);
-static CT build_multipart_alt (CT, CT, int, int);
+static CT build_multipart (CT, CT, int, int);
 static int boundary_in_content (FILE **, char *, const char *);
 static void transfer_noncontent_headers (CT, CT);
 static int set_ct_type (CT, int type, int subtype, int encoding);
@@ -115,7 +146,8 @@ static void update_cte (CT);
 static int least_restrictive_encoding (CT) PURE;
 static int less_restrictive (int, int);
 static int convert_charsets (CT, char *, int *);
-static int fix_always (CT, int *);
+static int fix_always (CT *, const fix_transformations *, int *);
+static int decode_header_field_bodies (CT, int *);
 static int fix_filename_param (char *, char *, PM *, PM *);
 static int fix_filename_encoding (CT);
 static int write_content (CT, const char *, char *, FILE *, int, int);
@@ -146,8 +178,10 @@ main (int argc, char **argv)
     fx.replacetextplain = 0;
     fx.decodetext = CE_8BIT;
     fx.decodetypes = "text,application/ics";  /* Default, per man page. */
+    fx.decodeheaderfieldbodies = NULL;
     fx.lf_line_endings = 0;
     fx.textcharset = NULL;
+    fx.checkbase64 = true;
 
     if (nmh_init(argv[0], true, false)) { return 1; }
 
@@ -198,6 +232,21 @@ main (int argc, char **argv)
                 }
                 fx.decodetypes = cp;
                 continue;
+            case DECODEHEADERFIELDBODIESSW:
+                if (! (cp = *argp++)  ||  *cp == '-') {
+                    die("missing argument to %s", argp[-2]);
+                }
+                fx.decodeheaderfieldbodies = cp;
+                if (strcasecmp (cp, "utf-8")  && strcasecmp (cp, "utf8")) {
+                    /* Because UTF-8 strings can't have embedded nulls.  Other
+                       encodings support that, too, but we won't bother to
+                       enumerate them. */
+                    die("-decodeheaderfieldbodies only supports utf-8");
+                }
+                continue;
+            case NDECODEHEADERFIELDBODIESSW:
+                fx.decodeheaderfieldbodies = NULL;
+                continue;
             case CRLFLINEBREAKSSW:
                 fx.lf_line_endings = 0;
                 continue;
@@ -213,6 +262,12 @@ main (int argc, char **argv)
             case NTEXTCHARSETSW:
                 fx.textcharset = 0;
                 continue;
+            case CHECKBASE64SW:
+                fx.checkbase64 = true;
+                continue;
+            case NCHECKBASE64SW:
+                fx.checkbase64 = false;
+                continue;
             case FIXBOUNDARYSW:
                 fx.fixboundary = 1;
                 continue;
@@ -558,7 +613,7 @@ mhfixmsgsbr (CT *ctp, char *maildir, const fix_transformations *fx,
     } /* else *outfp was defined by caller */
 
     reverse_alternative_parts (*ctp);
-    status = fix_always (*ctp, &message_mods);
+    status = fix_always (ctp, fx, &message_mods);
     if (status == OK  &&  fx->fixboundary) {
         status = fix_boundary (ctp, &message_mods);
     }
@@ -577,6 +632,9 @@ mhfixmsgsbr (CT *ctp, char *maildir, const fix_transformations *fx,
                                     &message_mods);
         update_cte (*ctp);
     }
+    if (status == OK  &&  fx->decodeheaderfieldbodies) {
+        status = decode_header_field_bodies(*ctp, &message_mods);
+    }
     if (status == OK  &&  fx->textcharset != NULL) {
         status = convert_charsets (*ctp, fx->textcharset, &message_mods);
     }
@@ -863,20 +921,20 @@ replace_boundary (CT ct, char *file, char *boundary)
             if (strcasecmp (TYPE_FIELD, np)) {
                 fprintf (fpout, "%s:%s", np, vp);
             } else {
-               char *new_ctline, *new_params;
+                char *new_ctline, *new_params;
 
-               replace_param(&ct->c_ctinfo.ci_first_pm,
-                             &ct->c_ctinfo.ci_last_pm, "boundary",
-                             boundary, 0);
+                replace_param(&ct->c_ctinfo.ci_first_pm,
+                              &ct->c_ctinfo.ci_last_pm, "boundary",
+                              boundary, 0);
 
-               new_ctline = concat(" ", ct->c_ctinfo.ci_type, "/",
-                                   ct->c_ctinfo.ci_subtype, NULL);
-               new_params = output_params(LEN(TYPE_FIELD) +
-                                          strlen(new_ctline) + 1,
-                                          ct->c_ctinfo.ci_first_pm, NULL, 0);
+                new_ctline = concat(" ", ct->c_ctinfo.ci_type, "/",
+                                    ct->c_ctinfo.ci_subtype, NULL);
+                new_params = output_params(LEN(TYPE_FIELD) +
+                                           strlen(new_ctline) + 1,
+                                           ct->c_ctinfo.ci_first_pm, NULL, 0);
                 fprintf (fpout, "%s:%s%s\n", np, new_ctline,
-                        FENDNULL(new_params));
-               free(new_ctline);
+                         FENDNULL(new_params));
+                free(new_ctline);
                 free(new_params);
             }
 
@@ -1140,7 +1198,6 @@ fix_composite_cte (CT ct, int *message_mods)
 
                     NEW(h);
                     h->name = mh_xstrdup (hf->name);
-                    h->hf_encoding = hf->hf_encoding;
                     h->next = hf->next;
                     hf->next = h;
 
@@ -1450,7 +1507,7 @@ find_textplain_sibling (CT parent, int replacetextplain,
 
 
 /*
- * Insert a new text/plain part.
+ * Insert a new text/plain part in a multipart part.
  */
 static int
 insert_new_text_plain_part (CT ct, int new_subpart_number, CT parent)
@@ -1524,12 +1581,13 @@ build_text_plain_part (CT encoded_part)
 static int
 insert_into_new_mp_alt (CT *ct, int *message_mods)
 {
+    /* The following will call decode_part(). */
     CT tp_part = build_text_plain_part (*ct);
     int status = OK;
 
     if (tp_part) {
-        CT mp_alt = build_multipart_alt (*ct, tp_part, CT_MULTIPART,
-                                         MULTI_ALTERNATE);
+        CT mp_alt = build_multipart (*ct, tp_part, CT_MULTIPART,
+                                     MULTI_ALTERNATE);
         if (mp_alt) {
             struct multipart *mp = (struct multipart *) mp_alt->c_ctparams;
 
@@ -1559,6 +1617,79 @@ insert_into_new_mp_alt (CT *ct, int *message_mods)
 }
 
 
+/*
+ * Slip new text/plain part into a new multipart/mixed.
+ */
+static int
+insert_into_new_mp_mixed (CT *ct, const char *content, int *message_mods)
+{
+    CT main_part = divide_part (*ct);
+    const char *reason = NULL;
+    const int encoding = content_encoding (main_part, &reason);
+    int status = OK;
+
+    if (set_ct_type(main_part, (*ct)->c_type, (*ct)->c_subtype,
+                    main_part->c_encoding) != OK) {
+        inform("failed to set Content-Type of main part");
+        return NOTOK;
+    }
+    if (set_ct_type(*ct, (*ct)->c_type, (*ct)->c_subtype, encoding) != OK) {
+        inform("failed to set Content-Type of new part");
+        return NOTOK;
+    }
+
+    if (main_part) {
+        /* Load remainder into the new part. */
+        CE cefile = &(*ct)->c_cefile;
+        CT mp_alt;
+
+        cefile->ce_file =
+            mh_xstrdup(m_mktemp2 (NULL, invo_name, NULL, &cefile->ce_fp));
+        if (cefile->ce_file == NULL) {
+            die("unable to create temporary file in %s", get_temp_dir());
+        }
+        cefile->ce_unlink = 1;
+        fprintf (cefile->ce_fp, "%s", content);
+
+        /* Put both parts into a new multipart. */
+        mp_alt = build_multipart (*ct, main_part, CT_MULTIPART, MULTI_MIXED);
+        if (mp_alt) {
+            struct multipart *mp = (struct multipart *) mp_alt->c_ctparams;
+
+            /* So fix_composite_cte doesn't try to overwrite the encoding.  If
+               the content needs to be decoded, c_encoding will be properly
+               set. */
+            mp_alt->c_encoding = encoding;
+
+            if (mp  &&  mp->mp_parts) {
+                mp->mp_parts->mp_part = main_part;
+                /* Make the new multipart/alternative the parent. */
+                *ct = mp_alt;
+
+                ++*message_mods;
+                if (verbosw) {
+                    report (NULL, (*ct)->c_partno, (*ct)->c_file,
+                            "insert text/plain part");
+                }
+            } else {
+                free_content (main_part);
+                free_content (mp_alt);
+                status = NOTOK;
+            }
+        } else {
+            inform("failed to build multipart/alternate");
+            status = NOTOK;
+        }
+    } else {
+        /* Should never happen. */
+        inform("failed to insert new text part into multipart/related");
+        status = NOTOK;
+    }
+
+    return status;
+}
+
+
 /*
  * Clone a MIME part.
  */
@@ -1602,13 +1733,13 @@ copy_ctinfo (CI dest, CI src)
     dest->ci_subtype = src->ci_subtype ? mh_xstrdup (src->ci_subtype) : NULL;
 
     for (s_pm = src->ci_first_pm; s_pm; s_pm = s_pm->pm_next) {
-       d_pm = add_param(&dest->ci_first_pm, &dest->ci_last_pm, s_pm->pm_name,
-                        s_pm->pm_value, 0);
-       if (s_pm->pm_charset) {
-           d_pm->pm_charset = mh_xstrdup(s_pm->pm_charset);
+        d_pm = add_param(&dest->ci_first_pm, &dest->ci_last_pm, s_pm->pm_name,
+                         s_pm->pm_value, 0);
+        if (s_pm->pm_charset) {
+            d_pm->pm_charset = mh_xstrdup(s_pm->pm_charset);
         }
-       if (s_pm->pm_lang) {
-           d_pm->pm_lang = mh_xstrdup(s_pm->pm_lang);
+        if (s_pm->pm_lang) {
+            d_pm->pm_lang = mh_xstrdup(s_pm->pm_lang);
         }
     }
 
@@ -1623,10 +1754,9 @@ copy_ctinfo (CI dest, CI src)
 static int
 decode_part (CT ct)
 {
-    char *tmp_decoded;
-    int status;
+    char *tempfile, *tmp_decoded;
     FILE *file;
-    char *tempfile;
+    int status;
 
     if ((tempfile = m_mktemp2 (NULL, invo_name, NULL, &file)) == NULL) {
         die("unable to create temporary file in %s", get_temp_dir());
@@ -1646,6 +1776,134 @@ decode_part (CT ct)
 }
 
 
+/*
+ * If base64-encoded content has a text trailer, return the location, relative
+ * to c->c_begin, where the valid base64 ends.  And return the trailer in the
+ * addresses pointed to by remainderp.  The caller is responsible for
+ * deallocating that.  If no text trailer, return ct->c_end - ct->c_begin and
+ * leave remainderp unchanged.
+ */
+static size_t
+get_valid_base64 (CT ct, char **remainderp) {
+    const size_t len = ct->c_end - ct->c_begin;
+    char *buf, format[16];
+    size_t pos;
+    int fd;
+
+    if (! ct->c_fp  &&  ((ct->c_fp = fopen (ct->c_file, "r")) == NULL)) {
+        advise (ct->c_file, "unable to open for reading");
+        return NOTOK;
+    }
+    if ((fd = fileno (ct->c_fp)) == -1  ||
+        lseek (fd, ct->c_begin, SEEK_SET) == (off_t) -1) {
+        advise (ct->c_file, "unable to seek in");
+        return NOTOK;
+    }
+    buf = mh_xmalloc(len + 1);
+    snprintf(format, sizeof format, "%%%luc", (unsigned long) len);
+    if (fscanf(ct->c_fp, format, buf) == EOF) {
+        advise (ct->c_file, "unable to read");
+        return NOTOK;
+    }
+    buf[len] = '\0';
+
+    pos = find_invalid_base64_pos(buf);
+
+    if (ct->c_begin + pos < (size_t) ct->c_end) {
+        *remainderp = mh_xstrdup(&buf[pos]);
+    } else {
+        pos = ct->c_end - ct->c_begin;
+    }
+    free(buf);
+
+    return pos;
+}
+
+
+/*
+ * Find position in byte string of invalid base64 code.  Skip individual
+ * invalid characters because RFC 2045 Sec 6.8 says they should be ignored.
+ * The motivating use case is a text footer that was mistakenly applied to
+ * base64 content.  Therefore, if any of these is found, return the position
+ * of:
+ * 1. The byte (or end) after one or two consecutive pad ('=') bytes.
+ * 2. The first of a pair of invalid base64 bytes.
+ *
+ * If the base64 code is valid, return the position of the null terminator.
+ *
+ * encoded      - the base64-encoded string
+ */
+static size_t
+find_invalid_base64_pos (const char *encoded) {
+    const char *cp;
+    size_t pos;
+    bool found_pad = false;
+    unsigned int found_invalid = 0;
+
+    for (cp = encoded, pos = 0;
+         *cp && ! found_pad && found_invalid < 2;
+         ++cp, ++pos) {
+        if (isspace ((unsigned char) *cp) ||
+            isalnum ((unsigned char) *cp) ||
+            *cp == '+' || *cp == '/' || *cp == '=') {
+            /* Valid base64 byte. */
+            if (*cp == '=') {
+                /* "evidence that the end of the data has been reached"
+                   according to RFC 2045 */
+                found_pad = true;
+            }
+            /* Require consecutive invalid bytes.  Let decodeBase64() handle
+               individual ones. */
+            found_invalid = 0;
+        } else {
+            ++found_invalid;
+        }
+    }
+
+    if (found_pad  &&  *cp  &&  *cp == '=') {
+        /* Skip over last in pair of ==. */
+        ++cp, ++pos;
+    } else if (found_invalid == 2) {
+        /* If a pair of consecutive invalid bytes, back up to first one. */
+        --cp, --pos;
+        --cp, --pos;
+    }
+
+    /* Skip over any trailing whitespace. */
+    while (*cp  &&  isspace((unsigned char) *cp)) {
+        ++cp, ++pos;
+    }
+
+    return pos;
+}
+
+
+/*
+ * Check for valid base64 encoding, and "fix" if invalid.
+ */
+static int
+check_base64_encoding (CT *ctp)
+{
+    char *remainder = NULL;
+    int status = OK;
+
+    /* If there's a footer after base64 content, set c_end to before it, and
+       store the footer in remainder. */
+    (*ctp)->c_end = (*ctp)->c_begin + get_valid_base64(*ctp, &remainder);
+
+    if (remainder != NULL) {
+        /* Move ct to a subpart of a new multipart/related, and add the
+           remainder as a new text/plain subpart of it. */
+        int ignore_message_mods = 0;
+
+        status = insert_into_new_mp_mixed(ctp, remainder, &ignore_message_mods);
+        free(remainder);
+    }
+
+    return status;
+}
+
+
 /*
  * Reformat content as plain text.
  * Some of the arguments aren't really needed now, but maybe will
@@ -1716,13 +1974,15 @@ reformat_part (CT ct, char *file, char *type, char *subtype, int c_type)
 
 
 /*
- * Fill in a multipart/alternative part.
+ * Create and fill in a multipart part.
  */
 static CT
-build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
+build_multipart (CT first_part, CT new_part, int type, int subtype)
 {
     char *boundary_prefix = "----=_nmh-multipart";
-    char *boundary = concat (boundary_prefix, first_alt->c_partno, NULL);
+    static unsigned int bp_uses = 0;
+    char bp_uses_buf[16];
+    char *boundary;
     char *boundary_indicator = "; boundary=";
     char *typename, *subtypename, *name;
     CT ct;
@@ -1732,7 +1992,15 @@ build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
 
     NEW0(ct);
 
-    /* Set up the multipart/alternative part.  These fields of *ct were
+    if (bp_uses > 0) {
+        snprintf(bp_uses_buf, sizeof bp_uses_buf - 1, "-%d", bp_uses++);
+    } else {
+        bp_uses_buf[0] = '\0';
+    }
+    boundary =
+        concat (boundary_prefix, bp_uses_buf, first_part->c_partno, NULL);
+
+    /* Set up the multipart part.  These fields of *ct were
        initialized to 0 by mh_xcalloc():
        c_fp, c_unlink, c_begin, c_end,
        c_vrsn, c_ctline, c_celine,
@@ -1745,7 +2013,7 @@ build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
        c_showproc, c_termproc, c_storeproc, c_storage, c_folder
     */
 
-    ct->c_file = mh_xstrdup (first_alt->c_file);
+    ct->c_file = mh_xstrdup (first_part->c_file);
     ct->c_type = type;
     ct->c_subtype = subtype;
 
@@ -1792,7 +2060,7 @@ build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
                 snprintf (buffer2, sizeof buffer2, "%d", serial);
                 boundary =
                     concat (boundary_prefix,
-                            FENDNULL(first_alt->c_partno),
+                            FENDNULL(first_part->c_partno),
                             "-", buffer2,  NULL);
             }
         }
@@ -1807,18 +2075,18 @@ build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
                    boundary, "\"", NULL);
 
     /* Load c_first_hf and c_last_hf. */
-    transfer_noncontent_headers (first_alt, ct);
+    transfer_noncontent_headers (first_part, ct);
     add_header (ct, mh_xstrdup (TYPE_FIELD), concat (name, "\n", NULL));
     free (name);
 
     /* Load c_partno. */
-    if (first_alt->c_partno) {
-        ct->c_partno = mh_xstrdup (first_alt->c_partno);
-        free (first_alt->c_partno);
-        first_alt->c_partno = concat (ct->c_partno, ".1", NULL);
+    if (first_part->c_partno) {
+        ct->c_partno = mh_xstrdup (first_part->c_partno);
+        free (first_part->c_partno);
+        first_part->c_partno = concat (ct->c_partno, ".1", NULL);
         new_part->c_partno = concat (ct->c_partno, ".2", NULL);
     } else {
-        first_alt->c_partno = mh_xstrdup ("1");
+        first_part->c_partno = mh_xstrdup ("1");
         new_part->c_partno = mh_xstrdup ("2");
     }
 
@@ -1833,7 +2101,7 @@ build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
     NEW(p);
     NEW(p->mp_next);
     p->mp_next->mp_next = NULL;
-    p->mp_next->mp_part = first_alt;
+    p->mp_next->mp_part = first_part;
 
     NEW0(m);
     m->mp_start = concat (boundary, "\n", NULL);
@@ -2528,37 +2796,37 @@ convert_charsets (CT ct, char *dest_charset, int *message_mods)
  *    headers, respectively.
  */
 static int
-fix_always (CT ct, int *message_mods)
+fix_always (CT *ctp, const fix_transformations *fx, int *message_mods)
 {
     int status = OK;
 
-    switch (ct->c_type) {
+    switch ((*ctp)->c_type) {
     case CT_MULTIPART: {
-        struct multipart *m = (struct multipart *) ct->c_ctparams;
+        struct multipart *m = (struct multipart *) (*ctp)->c_ctparams;
         struct part *part;
 
         for (part = m->mp_parts; status == OK  &&  part; part = part->mp_next) {
-            status = fix_always (part->mp_part, message_mods);
+            status = fix_always (&part->mp_part, fx, message_mods);
         }
         break;
     }
 
     case CT_MESSAGE:
-        if (ct->c_subtype == MESSAGE_EXTERNAL) {
-            struct exbody *e = (struct exbody *) ct->c_ctparams;
+        if ((*ctp)->c_subtype == MESSAGE_EXTERNAL) {
+            struct exbody *e = (struct exbody *) (*ctp)->c_ctparams;
 
-            status = fix_always (e->eb_content, message_mods);
+            status = fix_always (&e->eb_content, fx, message_mods);
         }
         break;
 
     default: {
         HF hf;
 
-        if (ct->c_first_hf) {
-            fix_filename_encoding (ct);
+        if ((*ctp)->c_first_hf) {
+            fix_filename_encoding (*ctp);
         }
 
-        for (hf = ct->c_first_hf; hf; hf = hf->next) {
+        for (hf = (*ctp)->c_first_hf; hf; hf = hf->next) {
             size_t len = strlen (hf->value);
 
             if (strcasecmp (hf->name, TYPE_FIELD) != 0  &&
@@ -2582,31 +2850,95 @@ fix_always (CT ct, int *message_mods)
                 hf->value[len - 1] = '\0';
 
                 /* Also, if Content-Type parameter, remove trailing ';'
-                   from ct->c_ctline.  This probably isn't necessary
+                   from (*ctp)->c_ctline.  This probably isn't necessary
                    but can't hurt. */
-                if (strcasecmp(hf->name, TYPE_FIELD) == 0 && ct->c_ctline) {
-                    size_t l = strlen(ct->c_ctline) - 1;
-                    while (isspace((unsigned char)(ct->c_ctline[l])) ||
-                           ct->c_ctline[l] == ';') {
-                        ct->c_ctline[l--] = '\0';
+                if (strcasecmp(hf->name, TYPE_FIELD) == 0 && (*ctp)->c_ctline) {
+                    size_t l = strlen((*ctp)->c_ctline) - 1;
+                    while (isspace((unsigned char)((*ctp)->c_ctline[l])) ||
+                           (*ctp)->c_ctline[l] == ';') {
+                        (*ctp)->c_ctline[l--] = '\0';
                         if (l == 0) { break; }
                     }
                 }
 
                 ++*message_mods;
                 if (verbosw) {
-                    report (NULL, ct->c_partno, ct->c_file,
+                    report (NULL, (*ctp)->c_partno, (*ctp)->c_file,
                             "remove trailing ; from %s parameter value",
                             hf->name);
                 }
             }
         }
+
+        if (fx->checkbase64  &&  (*ctp)->c_encoding == CE_BASE64) {
+            status = check_base64_encoding (ctp);
+        }
     }}
 
     return status;
 }
 
 
+/*
+ * Decodes UTF-8 encoded header values.  Similar to fix_filename_param(), but
+ * does not modify any MIME parameter values.
+ */
+static int
+decode_header_field_bodies (CT ct, int *message_mods)
+{
+    int status = OK;
+
+    switch (ct->c_type) {
+    case CT_MULTIPART: {
+        struct multipart *m = (struct multipart *) ct->c_ctparams;
+        struct part *part;
+
+        for (part = m->mp_parts; status == OK  &&  part; part = part->mp_next) {
+            status = decode_header_field_bodies (part->mp_part, message_mods);
+        }
+        break;
+    }
+
+    case CT_MESSAGE:
+        if (ct->c_subtype == MESSAGE_EXTERNAL) {
+            struct exbody *e = (struct exbody *) ct->c_ctparams;
+
+            status = decode_header_field_bodies (e->eb_content, message_mods);
+        }
+        break;
+    }
+
+    HF hf;
+
+    for (hf = ct->c_first_hf; hf; hf = hf->next) {
+        /* Only decode UTF-8 values. */
+        if (hf->value  &&  has_suffix(hf->value, "?=\n")  &&
+            (! strncasecmp (hf->value, " =?utf8?", 8)  ||
+             ! strncasecmp (hf->value, " =?utf-8?", 9))) {
+            /* Looks like an RFC 2047 encoded parameter. */
+            char decoded[PATH_MAX + 1];
+
+            if (decode_rfc2047 (hf->value, decoded, sizeof decoded)) {
+                const size_t len = strlen(decoded);
+
+                /* decode_rfc2047() could truncate if the buffer fills up.
+                   Detect and discard if that happened. */
+                if (len < sizeof(decoded) - 1  &&  strcmp(hf->value, decoded)) {
+                    hf->value = mh_xrealloc (hf->value, len + 1);
+                    strncpy (hf->value, decoded, len + 1);
+                    ++*message_mods;
+                }
+            } else {
+                inform("failed to decode %s parameter %s", hf->name, hf->value);
+                status = NOTOK;
+            }
+        }
+    }
+
+    return status;
+}
+
+
 /*
  * Factor out common code for loops in fix_filename_encoding().
  */