From 81473f879b34e146af19e000a0aec8c75bc03548 Mon Sep 17 00:00:00 2001 From: "Steffen (Daode) Nurpmeso" Date: Fri, 11 Jul 2014 21:42:02 +0200 Subject: [PATCH] Add *pipe-EXTENSION*, ext. *mime-counter-evidence* (Bob Tennent).. Outsource and generalize the fetching of external handlers into the new mimepart_get_handler() function and add a *pipe-EXTENSION* mechanism that takes precedence before *pipe-CONTENT/SUBCONTENT*. While here, go the first step in extending *mime-counter-evidence* and convert it to a value option, let bit 2 mean "don't forget the counter-evidence, use it to lookup the respective *pipe-CONTENT/SUBCONTENT*. Reported and requested by Bob Tennent (rdt AT cs DOT queensu DOT ca). While doing this i realized and fixed two further bugs: - The `[Directly address message only to display this]' code path would have printed a plain text part regardless of an installed pipe handler. - Pass through ASYNC flag correctly. --- mime.c | 76 ++++++++++++++++++++++++++++++++---- nail.1 | 129 +++++++++++++++++++++++++++++++++++++------------------------ nail.h | 8 +++- nailfuns.h | 10 +++-- send.c | 94 +++++++++++++++++++------------------------- 5 files changed, 202 insertions(+), 115 deletions(-) diff --git a/mime.c b/mime.c index 515de142..05174f6a 100644 --- a/mime.c +++ b/mime.c @@ -1120,26 +1120,32 @@ jleave: } FL enum mimecontent -mime_classify_content_of_part(struct mimepart const *mip) +mime_classify_content_of_part(struct mimepart *mpp) { enum mimecontent mc; char const *ct; + union {char const *cp; long l;} mce; NYD_ENTER; mc = MIME_UNKNOWN; - ct = mip->m_ct_type_plain; + ct = mpp->m_ct_type_plain; - if (!asccasecmp(ct, "application/octet-stream") && - mip->m_filename != NULL && ok_blook(mime_counter_evidence)) { - ct = mime_classify_content_type_by_fileext(mip->m_filename); + if (!asccasecmp(ct, "application/octet-stream") && mpp->m_filename != NULL && + (mce.cp = ok_vlook(mime_counter_evidence)) != NULL) { + ct = mime_classify_content_type_by_fileext(mpp->m_filename); if (ct == NULL) - /* TODO how about let *mime-counter-evidence* have - * TODO a value, and if set, saving the attachment in + /* TODO add bit 1 to possible *mime-counter-evidence* value + * TODO and let it mean to save the attachment in * TODO a temporary file that mime_classify_file() can * TODO examine, and using MIME_TEXT if that gives us * TODO something that seems to be human readable?! */ goto jleave; + + mce.l = strtol(mce.cp, NULL, 0); + if (mce.l & MIMECE_USR_OVWR) + mpp->m_ct_type_usr_ovwr = UNCONST(ct); } + if (strchr(ct, '/') == NULL) /* For compatibility with non-MIME */ mc = MIME_TEXT; else if (!asccasecmp(ct, "text/plain")) @@ -1176,6 +1182,8 @@ mime_classify_content_type_by_fileext(char const *name) /* TODO mime_classify(): mime.types(5) has *-gz but we search dot! * TODO i.e., we cannot handle files like dubidu.tar-gz; need globs! */ + /* TODO even better: regex, with fast lists for (README|INSTALL|NEWS) etc, + * TODO that, also add some mechanism for filenames without extension */ if ((name = strrchr(name, '.')) == NULL || *++name == '\0') goto jleave; @@ -1207,6 +1215,60 @@ jleave: return content; } +FL char * +mimepart_get_handler(struct mimepart const *mpp) +{ +#define __S "pipe-" +#define __L (sizeof(__S) -1) + char const *es, *cs; + size_t el, cl, l; + char *buf, *rv; + NYD_ENTER; + + /* TODO some mechanism for filenames without extension */ + el = ((es = mpp->m_filename) != NULL && (es = strrchr(es, '.')) != NULL && + *++es != '\0') ? strlen(es) : 0; + cl = ((cs = mpp->m_ct_type_usr_ovwr) != NULL || + (cs = mpp->m_ct_type_plain) != NULL) ? strlen(cs) : 0; + if ((l = MAX(el, cl)) == 0) { + rv = NULL; + goto jleave; + } + + buf = ac_alloc(__L + l +1); + memcpy(buf, __S, __L); + + /* File-extension handlers take precedence. + * Yes, we really "fail" here for file extensions which clash MIME types */ + if (el > 0) { + memcpy(buf + __L, es, el +1); + for (rv = buf + __L; *rv != '\0'; ++rv) + *rv = lowerconv(*rv); + + if ((rv = vok_vlook(buf)) != NULL) + goto jok; + } + + /* Then MIME Content-Type: */ + if (cl > 0) { + memcpy(buf + __L, cs, cl +1); + for (rv = buf + __L; *rv != '\0'; ++rv) + *rv = lowerconv(*rv); + + if ((rv = vok_vlook(buf)) != NULL) + goto jok; + } + + rv = NULL; +jok: + ac_free(buf); +jleave: + NYD_LEAVE; + return rv; +#undef __L +#undef __S +} + FL int c_mimetypes(void *v) { diff --git a/nail.1 b/nail.1 index 96a91c4c..b6919779 100644 --- a/nail.1 +++ b/nail.1 @@ -434,6 +434,57 @@ Deleted messages will, however, usually disappear never to be seen again. .\" }}} .\" +.\" .Ss "Viewing HTML mail and MIME attachments" {{{ +.Ss "Viewing HTML mail and MIME attachments" +Messages which are HTML-only get more and more common and of course many +messages come bundled with a bouquet of MIME attachments. +\*(UA can't deal with any of these itself, but instead programs need to +become registered to deal with specific MIME types or file extensions; +these programs may either prepare a plain text version of its input, +i.e., in order to enable \*(UA to display the content on the terminal +(or, as necessary and desired, through +.Ev PAGER Ns +), or display the content themselves, for example in a graphical window. +The latter type of programs by default "suspends" \*(UA until the +external viewer is terminated, but asynchronous side-by-side execution +is also possible, in which case \*(UA will continue to display the +message and remain responsive. +.Pp +To install an external handler program for a specific MIME type, set a +.Va pipe-CONTENT/SUBCONTENT +variable accordingly. +To define a handler for a specific file extension set the respective +.Va pipe-EXTENSION +variable \(en these handlers take precedence. +The variable +.Va mime-counter-evidence +can be set to improve dealing with faulty MIME part declarations as are +often seen in real-life messages. +E.g., to display a HTML message inline (that is, converted to plain +text) with either of the text-mode browsers +.Xr lynx 1 +or +.Xr elinks 1 , +and to open PDF attachments in an external PDF viewer, asynchronously: +.Bd -literal -offset indent + #set pipe-text/html="elinks -force-html -dump 1" + set pipe-text/html="lynx -stdin -dump -force_html" + set pipe-application/pdf="@&cat >/tmp/\*(ua$$.pdf; \e + acroread /tmp/\*(ua$$.pdf; rm /tmp/\*(ua$$.pdf" +.Ed +.Pp +Note: special care must be taken when using such commands as mail +viruses may be distributed by this method; +if messages of type `application/x-sh' or files with the extensions `sh' +were blindly filtered through the shell, for example, a message sender +could easily execute arbitrary code on the system \*(UA is running on. +For more on MIME, also in respect to sending of messages, see the +section +.Sx "MIME types" +and the command +.Ic mimetypes . +.\" }}} +.\" .\" .Ss "Replying to or originating mail" {{{ .Ss "Replying to or originating mail" The command @@ -3043,16 +3094,6 @@ automatically interpret the contents of the part. If this option is set, and the data was unambiguously identified as text data at first glance (by a `.txt' or `.html' file extension), then the original `Content-Type:' will not be overwritten. -.It Va mime-counter-evidence -Normally the `Content-Type:' field is used to decide how to treat -a messages MIME part. -Some MUAs however don't use -.Xr mime.types 5 -or a similar mechanism to correctly classify content, -but simply specify `application/octet-stream', -even for plain text attachments like `text/diff'. -If this variable is set then \*(UA will use the file extension of -attachments to classify such MIME message parts, if possible. .It Va netrc-lookup \*(IN \*(OP Used to control usage of the users .Pa ~/.netrc @@ -3690,6 +3731,25 @@ for the command and the .Va folder option. +.It Va mime-counter-evidence +Normally the `Content-Type:' field is used to decide how to treat +a messages MIME part. +Some MUAs however don't use +.Xr mime.types 5 +or a similar mechanism to correctly classify content, +but simply specify `application/octet-stream', +even for plain text attachments like `text/diff'. +If this variable is set then \*(UA will use the file extension of +attachments to classify such MIME message parts, if possible. +.Pp +This can also be given a non-empty value: in this case the value is +expected to be a number, actually a carrier of bits. +If bit two is set (0x2) then the detected "real" content-type will be +carried along with the message and be used for detecting which +.Va pipe-CONTENT/SUBCONTENT +is responsible for the MIME part, shall that question arise; +when displaying such a MIME part the part-info will indicate the +overridden content-type by showing a plus-sign (`+'). .It Va mimetypes-load-control This option can be used to control which of the .Xr mime.types 5 @@ -3781,9 +3841,9 @@ If no such variable is defined for a host, the user will be asked for a password on standard input. Specifying passwords in a startup file is generally a security risk; the file should be readable by the invoking user only. -.It Va pipe-content/subcontent -When a MIME message part of `content/subcontent' type is displayed or -is replied to, +.It Va pipe-CONTENT/SUBCONTENT +When a MIME message part of `CONTENT/SUBCONTENT' (normalized to +lowercase) type is displayed or is replied to, its text is filtered through the value of this variable interpreted as a shell command. .Pp @@ -3801,13 +3861,12 @@ addition to what has been described for the plain `@' shell command prefix, the command will be run asynchronously, i.e., without blocking \*(UA, which may be a handy way to display a, e.g., PDF file while also continuing to read the mail message. -.Pp -Special care must be taken when using such commands as mail viruses may -be distributed by this method; -if messages of type `application/x-sh' were filtered through the shell, -for example, -a message sender could easily execute arbitrary code on the system \*(UA -is running on. +.It Va pipe-EXTENSION +This is identical to +.Va pipe-CONTENT/SUBCONTENT +except that `EXTENSION' (normalized to lowercase using character +mappings of the ASCII charset) names a file extension, e.g., `xhtml'. +Handlers registered using this method take precedence. .It Va pop3-keepalive \*(OP POP3 servers close the connection after a period of inactivity; the standard requires this to be at least 10 minutes, @@ -4896,36 +4955,6 @@ One should use the `-verify' and `-CAfile' options of to be "on the safe side" regarding the fetched certificates. .\" }}} .\" -.\" .Ss "Reading HTML mail" {{{ -.Ss "Reading HTML mail" -You need either the -.Xr elinks 1 -or -.Xr lynx 1 -utility or another command-line web browser that can write plain text to -standard output. -.Pp -.Dl set pipe-text/html="elinks -force-html -dump 1" -.Dl set pipe-text/html="lynx -stdin -dump -force_html" -.Pp -will cause HTML message parts to be converted into a more friendly form. -.\" }}} -.\" -.\" .Ss "Viewing PDF attachments" {{{ -.Ss "Viewing PDF attachments" -Most PDF viewers do not accept input directly from a pipe. -It is thus necessary to store the attachment in a temporary file first: -.Pp -.Dl set pipe-application/pdf="@&cat >/tmp/\*(ua$$.pdf; \e -.Dl \ \ \ \ \ \ acroread /tmp/\*(ua$$.pdf; rm /tmp/\*(ua$$.pdf" -.Pp -Note that security defects are discovered in PDF viewers from time to -time. -Automatical command execution like this can compromise your system -security, -in particular if you stay not always informed about such issues. -.\" }}} -.\" .\" .Ss "Signed and encrypted messages with S/MIME" {{{ .Ss "Signed and encrypted messages with S/MIME" \*(OP S/MIME provides two central mechanisms: diff --git a/nail.h b/nail.h index c14a287d..5d4d67b5 100644 --- a/nail.h +++ b/nail.h @@ -592,6 +592,11 @@ enum mimeenc { MIME_B64 /* message is in base64 encoding */ }; +enum mime_counter_evidence { + MIMECE_NONE, + MIMECE_USR_OVWR = 1<<1 +}; + enum conversion { CONV_NONE, /* no conversion */ CONV_7BIT, /* no conversion, is 7bit */ @@ -794,7 +799,6 @@ enum okeys { ok_b_message_id_disable, ok_b_metoo, ok_b_mime_allow_text_controls, - ok_b_mime_counter_evidence, ok_b_netrc_lookup, ok_b_outfolder, ok_b_page, @@ -867,6 +871,7 @@ enum okeys { ok_v_LISTER, ok_v_MAIL, ok_v_MBOX, + ok_v_mime_counter_evidence, /* TODO v15-compat: mimetypes-load-control -> mimetypes-load / mimetypes */ ok_v_mimetypes_load_control, ok_v_NAIL_EXTRA_RC, /* {name=NAIL_EXTRA_RC} */ @@ -1196,6 +1201,7 @@ struct mimepart { struct mimepart *m_parent; /* enclosing multipart part */ char *m_ct_type; /* content-type */ char *m_ct_type_plain; /* content-type without specs */ + char *m_ct_type_usr_ovwr; /* Forcefully overwritten one */ enum mimecontent m_mimecontent; /* same in enum */ char const *m_charset; /* charset */ char *m_ct_transfer_enc; /* content-transfer-encoding */ diff --git a/nailfuns.h b/nailfuns.h index bfffbc48..13cf81eb 100644 --- a/nailfuns.h +++ b/nailfuns.h @@ -1184,13 +1184,17 @@ FL char * mime_create_boundary(void); FL int mime_classify_file(FILE *fp, char const **contenttype, char const **charset, int *do_iconv); -/* */ -FL enum mimecontent mime_classify_content_of_part(struct mimepart const *mip); +/* Dependend on *mime-counter-evidence* mpp->m_ct_type_usr_ovwr will be set, + * but otherwise mpp is const */ +FL enum mimecontent mime_classify_content_of_part(struct mimepart *mpp); /* Return the Content-Type matching the extension of name */ FL char * mime_classify_content_type_by_fileext(char const *name); -/* "mimetypes" command */ +/* Get the (pipe) handler for a part, or NULL if there is none known */ +FL char * mimepart_get_handler(struct mimepart const *mpp); + +/* `mimetypes' command */ FL int c_mimetypes(void *v); /* Convert header fields from RFC 1522 format */ diff --git a/send.c b/send.c index 10ca25fd..f8d7b54b 100644 --- a/send.c +++ b/send.c @@ -77,8 +77,8 @@ static void __endpart(struct mimepart **np, off_t xoffs, long lines); static void _print_part_info(struct str *out, struct mimepart *mip, struct ignoretab *doign, int level); -/* Query possible pipe command for MIME type */ -static enum pipeflags _pipecmd(char **result, char const *content_type); +/* Query possible pipe command for MIME part */ +static enum pipeflags _pipecmd(char **result, struct mimepart const *mpp); /* Create a pipe */ static FILE * _pipefile(char const *pipecomm, FILE **qbuf, bool_t quote, @@ -159,6 +159,7 @@ parsepart(struct message *zmp, struct mimepart *ip, enum parseflags pf, ip->m_ct_type_plain = UNCONST("message/rfc822"); else ip->m_ct_type_plain = UNCONST("text/plain"); + ip->m_ct_type_usr_ovwr = NULL; if (ip->m_ct_type != NULL) ip->m_charset = mime_getparam("charset", ip->m_ct_type); @@ -438,20 +439,33 @@ _print_part_info(struct str *out, struct mimepart *mip, /* Max. 24 */ if (is_ign("content-type", 12, doign)) { - out->s = mip->m_ct_type_plain; + size_t addon; + + if ((out->s = mip->m_ct_type_usr_ovwr) != NULL) + addon = 2; + else { + addon = 0; + out->s = mip->m_ct_type_plain; + } out->l = strlen(out->s); - ct.s = ac_alloc(out->l + 2 +1); + + ct.s = ac_alloc(out->l + 2 + addon +1); ct.s[0] = ','; ct.s[1] = ' '; ct.l = 2; + if (addon) { + ct.s[ct.l++] = '+'; + ct.s[ct.l++] = ' '; + } + if (is_prefix("application/", out->s)) { - memcpy(ct.s + 2, "appl../", 7); + memcpy(ct.s + ct.l, "appl../", 7); ct.l += 7; out->l -= 12; out->s += 12; - out->l = MIN(out->l, 17); + out->l = MIN(out->l, 17 - addon); } else - out->l = MIN(out->l, 24); + out->l = MIN(out->l, 24 - addon); memcpy(ct.s + ct.l, out->s, out->l); ct.l += out->l; ct.s[ct.l] = '\0'; @@ -512,34 +526,19 @@ _print_part_info(struct str *out, struct mimepart *mip, } static enum pipeflags -_pipecmd(char **result, char const *content_type) +_pipecmd(char **result, struct mimepart const *mpp) { enum pipeflags ret; - char *s, *cp; - char const *cq; + char *cp; NYD_ENTER; - ret = PIPE_NULL; - *result = NULL; - if (content_type == NULL) - goto jleave; - - /* First check wether there is a special pipe-MIMETYPE handler */ - s = ac_alloc(strlen(content_type) + 5 +1); - memcpy(s, "pipe-", 5); - cp = s + 5; - cq = content_type; - do - *cp++ = lowerconv(*cq); - while (*cq++ != '\0'); - cp = vok_vlook(s); - ac_free(s); - - if (cp == NULL) - goto jleave; - + /* Do we have any handler for this part? */ + if ((cp = mimepart_get_handler(mpp)) == NULL) { + ret = PIPE_NULL; + *result = NULL; + } /* User specified a command, inspect for special cases */ - if (cp[0] != '@') { + else if (cp[0] != '@') { /* Normal command line */ ret = PIPE_COMM; *result = cp; @@ -552,31 +551,16 @@ _pipecmd(char **result, char const *content_type) *result = UNCONST(_("[Directly address message only to display this]\n")); } else { /* Viewing a single message only */ -#if 0 /* TODO send/MIME layer rewrite: when we have a single-pass parser - * TODO then the parsing phase and the send phase will be separated; - * TODO that allows us to ask a user *before* we start the send, i.e., - * TODO *before* a pager pipe is setup (which is the problem with - * TODO the '#if 0' code here) */ - size_t l = strlen(content_type); - char const *x = _("Should i display a part `%s' (y/n)? "); - s = ac_alloc(l += strlen(x) +1); - snprintf(s, l - 1, x, content_type); - l = getapproval(s), TRU1; - puts(""); /* .. we've hijacked a pipe 8-] ... */ - ac_free(s); - if (!l) { - x = _("[User skipped diplay]\n"); - ret = PIPE_MSG; - *result = UNCONST(x); - } else -#endif + /* TODO send/MIME layer rewrite: when we have a single-pass parser + * TODO then the parsing phase and the send phase will be separated; + * TODO that allows us to ask a user *before* we start the send, i.e., + * TODO *before* a pager pipe is setup */ if (cp[0] == '&') /* Asynchronous command, normal command line */ ret = PIPE_ASYNC, *result = ++cp; else ret = PIPE_COMM, *result = cp; } -jleave: NYD_LEAVE; return ret; } @@ -935,17 +919,19 @@ jskip: case SEND_QUOTE: case SEND_QUOTE_ALL: ispipe = TRU1; - switch (_pipecmd(&pipecomm, ip->m_ct_type_plain)) { + switch (_pipecmd(&pipecomm, ip)) { case PIPE_MSG: _out(pipecomm, strlen(pipecomm), obuf, CONV_NONE, SEND_MBOX, qf, stats, NULL); - pipecomm = NULL; - /* FALLTRHU */ + /* We would print this as plain text, so better force going home */ + goto jleave; case PIPE_TEXT: case PIPE_COMM: - case PIPE_ASYNC: case PIPE_NULL: break; + case PIPE_ASYNC: + ispipe = FAL0; + break; } /* FALLTRHU */ default: @@ -968,7 +954,7 @@ jskip: case SEND_QUOTE: case SEND_QUOTE_ALL: ispipe = TRU1; - switch (_pipecmd(&pipecomm, ip->m_ct_type_plain)) { + switch (_pipecmd(&pipecomm, ip)) { case PIPE_MSG: _out(pipecomm, strlen(pipecomm), obuf, CONV_NONE, SEND_MBOX, qf, stats, NULL); -- 2.11.4.GIT