1 /**
2  * @file
3  * RFC1524 Mailcap routines
4  *
5  * @authors
6  * Copyright (C) 1996-2000,2003,2012 Michael R. Elkins <me@mutt.org>
7  *
8  * @copyright
9  * This program is free software: you can redistribute it and/or modify it under
10  * the terms of the GNU General Public License as published by the Free Software
11  * Foundation, either version 2 of the License, or (at your option) any later
12  * version.
13  *
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17  * details.
18  *
19  * You should have received a copy of the GNU General Public License along with
20  * this program.  If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 /**
24  * @page neo_mailcap RFC1524 Mailcap routines
25  *
26  * RFC1524 defines a format for the Multimedia Mail Configuration, which is the
27  * standard mailcap file format under Unix which specifies what external
28  * programs should be used to view/compose/edit multimedia files based on
29  * content type.
30  *
31  * This file contains various functions for implementing a fair subset of
32  * RFC1524.
33  */
34 
35 #include "config.h"
36 #include <stdbool.h>
37 #include <stdio.h>
38 #include <string.h>
39 #include "mutt/lib.h"
40 #include "config/lib.h"
41 #include "email/lib.h"
42 #include "core/lib.h"
43 #include "mailcap.h"
44 #include "attach/lib.h"
45 #include "muttlib.h"
46 #include "protos.h"
47 
48 /**
49  * mailcap_expand_command - Expand expandos in a command
50  * @param a        Email Body
51  * @param filename File containing the email text
52  * @param type     Type, e.g. "text/plain"
53  * @param command  Buffer containing command
54  * @retval 0 Command works on a file
55  * @retval 1 Command works on a pipe
56  *
57  * The command semantics include the following:
58  * %s is the filename that contains the mail body data
59  * %t is the content type, like text/plain
60  * %{parameter} is replaced by the parameter value from the content-type field
61  * \% is %
62  * Unsupported rfc1524 parameters: these would probably require some doing
63  * by neomutt, and can probably just be done by piping the message to metamail
64  * %n is the integer number of sub-parts in the multipart
65  * %F is "content-type filename" repeated for each sub-part
66  */
mailcap_expand_command(struct Body * a,const char * filename,const char * type,struct Buffer * command)67 int mailcap_expand_command(struct Body *a, const char *filename,
68                            const char *type, struct Buffer *command)
69 {
70   int needspipe = true;
71   struct Buffer *buf = mutt_buffer_pool_get();
72   struct Buffer *quoted = mutt_buffer_pool_get();
73   struct Buffer *param = NULL;
74   struct Buffer *type2 = NULL;
75 
76   const bool c_mailcap_sanitize =
77       cs_subset_bool(NeoMutt->sub, "mailcap_sanitize");
78   const char *cptr = mutt_buffer_string(command);
79   while (*cptr)
80   {
81     if (*cptr == '\\')
82     {
83       cptr++;
84       if (*cptr)
85         mutt_buffer_addch(buf, *cptr++);
86     }
87     else if (*cptr == '%')
88     {
89       cptr++;
90       if (*cptr == '{')
91       {
92         const char *pvalue2 = NULL;
93 
94         if (param)
95           mutt_buffer_reset(param);
96         else
97           param = mutt_buffer_pool_get();
98 
99         /* Copy parameter name into param buffer */
100         cptr++;
101         while (*cptr && (*cptr != '}'))
102           mutt_buffer_addch(param, *cptr++);
103 
104         /* In send mode, use the current charset, since the message hasn't
105          * been converted yet.   If noconv is set, then we assume the
106          * charset parameter has the correct value instead. */
107         if (mutt_istr_equal(mutt_buffer_string(param), "charset") && a->charset && !a->noconv)
108           pvalue2 = a->charset;
109         else
110           pvalue2 = mutt_param_get(&a->parameter, mutt_buffer_string(param));
111 
112         /* Now copy the parameter value into param buffer */
113         if (c_mailcap_sanitize)
114           mutt_buffer_sanitize_filename(param, NONULL(pvalue2), false);
115         else
116           mutt_buffer_strcpy(param, NONULL(pvalue2));
117 
118         mutt_buffer_quote_filename(quoted, mutt_buffer_string(param), true);
119         mutt_buffer_addstr(buf, mutt_buffer_string(quoted));
120       }
121       else if ((*cptr == 's') && filename)
122       {
123         mutt_buffer_quote_filename(quoted, filename, true);
124         mutt_buffer_addstr(buf, mutt_buffer_string(quoted));
125         needspipe = false;
126       }
127       else if (*cptr == 't')
128       {
129         if (!type2)
130         {
131           type2 = mutt_buffer_pool_get();
132           if (c_mailcap_sanitize)
133             mutt_buffer_sanitize_filename(type2, type, false);
134           else
135             mutt_buffer_strcpy(type2, type);
136         }
137         mutt_buffer_quote_filename(quoted, mutt_buffer_string(type2), true);
138         mutt_buffer_addstr(buf, mutt_buffer_string(quoted));
139       }
140 
141       if (*cptr)
142         cptr++;
143     }
144     else
145       mutt_buffer_addch(buf, *cptr++);
146   }
147   mutt_buffer_copy(command, buf);
148 
149   mutt_buffer_pool_release(&buf);
150   mutt_buffer_pool_release(&quoted);
151   mutt_buffer_pool_release(&param);
152   mutt_buffer_pool_release(&type2);
153 
154   return needspipe;
155 }
156 
157 /**
158  * get_field - NUL terminate a RFC1524 field
159  * @param s String to alter
160  * @retval ptr  Start of next field
161  * @retval NULL Error
162  */
get_field(char * s)163 static char *get_field(char *s)
164 {
165   if (!s)
166     return NULL;
167 
168   char *ch = NULL;
169 
170   while ((ch = strpbrk(s, ";\\")))
171   {
172     if (*ch == '\\')
173     {
174       s = ch + 1;
175       if (*s)
176         s++;
177     }
178     else
179     {
180       *ch = '\0';
181       ch = mutt_str_skip_email_wsp(ch + 1);
182       break;
183     }
184   }
185   mutt_str_remove_trailing_ws(s);
186   return ch;
187 }
188 
189 /**
190  * get_field_text - Get the matching text from a mailcap
191  * @param field    String to parse
192  * @param entry    Save the entry here
193  * @param type     Type, e.g. "text/plain"
194  * @param filename Mailcap filename
195  * @param line     Mailcap line
196  * @retval 1 Success
197  * @retval 0 Failure
198  */
get_field_text(char * field,char ** entry,const char * type,const char * filename,int line)199 static int get_field_text(char *field, char **entry, const char *type,
200                           const char *filename, int line)
201 {
202   field = mutt_str_skip_whitespace(field);
203   if (*field == '=')
204   {
205     if (entry)
206     {
207       field++;
208       field = mutt_str_skip_whitespace(field);
209       mutt_str_replace(entry, field);
210     }
211     return 1;
212   }
213   else
214   {
215     mutt_error(_("Improperly formatted entry for type %s in \"%s\" line %d"),
216                type, filename, line);
217     return 0;
218   }
219 }
220 
221 /**
222  * rfc1524_mailcap_parse - Parse a mailcap entry
223  * @param a        Email Body
224  * @param filename Filename
225  * @param type     Type, e.g. "text/plain"
226  * @param entry    Entry, e.g. "compose"
227  * @param opt      Option, see #MailcapLookup
228  * @retval true  Success
229  * @retval false Failure
230  */
rfc1524_mailcap_parse(struct Body * a,const char * filename,const char * type,struct MailcapEntry * entry,enum MailcapLookup opt)231 static bool rfc1524_mailcap_parse(struct Body *a, const char *filename, const char *type,
232                                   struct MailcapEntry *entry, enum MailcapLookup opt)
233 {
234   char *buf = NULL;
235   bool found = false;
236   int line = 0;
237 
238   /* rfc1524 mailcap file is of the format:
239    * base/type; command; extradefs
240    * type can be * for matching all
241    * base with no /type is an implicit wild
242    * command contains a %s for the filename to pass, default to pipe on stdin
243    * extradefs are of the form:
244    *  def1="definition"; def2="define \;";
245    * line wraps with a \ at the end of the line
246    * # for comments */
247 
248   /* find length of basetype */
249   char *ch = strchr(type, '/');
250   if (!ch)
251     return false;
252   const int btlen = ch - type;
253 
254   FILE *fp = fopen(filename, "r");
255   if (fp)
256   {
257     size_t buflen;
258     while (!found && (buf = mutt_file_read_line(buf, &buflen, fp, &line, MUTT_RL_CONT)))
259     {
260       /* ignore comments */
261       if (*buf == '#')
262         continue;
263       mutt_debug(LL_DEBUG2, "mailcap entry: %s\n", buf);
264 
265       /* check type */
266       ch = get_field(buf);
267       if (!mutt_istr_equal(buf, type) && (!mutt_istrn_equal(buf, type, btlen) ||
268                                           ((buf[btlen] != '\0') && /* implicit wild */
269                                            !mutt_str_equal(buf + btlen, "/*")))) /* wildsubtype */
270       {
271         continue;
272       }
273 
274       /* next field is the viewcommand */
275       char *field = ch;
276       ch = get_field(ch);
277       if (entry)
278         entry->command = mutt_str_dup(field);
279 
280       /* parse the optional fields */
281       found = true;
282       bool copiousoutput = false;
283       bool composecommand = false;
284       bool editcommand = false;
285       bool printcommand = false;
286 
287       while (ch)
288       {
289         field = ch;
290         ch = get_field(ch);
291         mutt_debug(LL_DEBUG2, "field: %s\n", field);
292         size_t plen;
293 
294         if (mutt_istr_equal(field, "needsterminal"))
295         {
296           if (entry)
297             entry->needsterminal = true;
298         }
299         else if (mutt_istr_equal(field, "copiousoutput"))
300         {
301           copiousoutput = true;
302           if (entry)
303             entry->copiousoutput = true;
304         }
305         else if ((plen = mutt_istr_startswith(field, "composetyped")))
306         {
307           /* this compare most occur before compose to match correctly */
308           if (get_field_text(field + plen, entry ? &entry->composetypecommand : NULL,
309                              type, filename, line))
310           {
311             composecommand = true;
312           }
313         }
314         else if ((plen = mutt_istr_startswith(field, "compose")))
315         {
316           if (get_field_text(field + plen, entry ? &entry->composecommand : NULL,
317                              type, filename, line))
318           {
319             composecommand = true;
320           }
321         }
322         else if ((plen = mutt_istr_startswith(field, "print")))
323         {
324           if (get_field_text(field + plen, entry ? &entry->printcommand : NULL,
325                              type, filename, line))
326           {
327             printcommand = true;
328           }
329         }
330         else if ((plen = mutt_istr_startswith(field, "edit")))
331         {
332           if (get_field_text(field + plen, entry ? &entry->editcommand : NULL,
333                              type, filename, line))
334           {
335             editcommand = true;
336           }
337         }
338         else if ((plen = mutt_istr_startswith(field, "nametemplate")))
339         {
340           get_field_text(field + plen, entry ? &entry->nametemplate : NULL,
341                          type, filename, line);
342         }
343         else if ((plen = mutt_istr_startswith(field, "x-convert")))
344         {
345           get_field_text(field + plen, entry ? &entry->convert : NULL, type, filename, line);
346         }
347         else if ((plen = mutt_istr_startswith(field, "test")))
348         {
349           /* This routine executes the given test command to determine
350            * if this is the right entry.  */
351           char *test_command = NULL;
352 
353           if (get_field_text(field + plen, &test_command, type, filename, line) && test_command)
354           {
355             struct Buffer *command = mutt_buffer_pool_get();
356             struct Buffer *afilename = mutt_buffer_pool_get();
357             mutt_buffer_strcpy(command, test_command);
358             const bool c_mailcap_sanitize =
359                 cs_subset_bool(NeoMutt->sub, "mailcap_sanitize");
360             if (c_mailcap_sanitize)
361               mutt_buffer_sanitize_filename(afilename, NONULL(a->filename), true);
362             else
363               mutt_buffer_strcpy(afilename, NONULL(a->filename));
364             mailcap_expand_command(a, mutt_buffer_string(afilename), type, command);
365             if (mutt_system(mutt_buffer_string(command)))
366             {
367               /* a non-zero exit code means test failed */
368               found = false;
369             }
370             FREE(&test_command);
371             mutt_buffer_pool_release(&command);
372             mutt_buffer_pool_release(&afilename);
373           }
374         }
375         else if (mutt_istr_startswith(field, "x-neomutt-keep"))
376         {
377           if (entry)
378             entry->xneomuttkeep = true;
379         }
380         else if (mutt_istr_startswith(field, "x-neomutt-nowrap"))
381         {
382           if (entry)
383             entry->xneomuttnowrap = true;
384           a->nowrap = true;
385         }
386       } /* while (ch) */
387 
388       if (opt == MUTT_MC_AUTOVIEW)
389       {
390         if (!copiousoutput)
391           found = false;
392       }
393       else if (opt == MUTT_MC_COMPOSE)
394       {
395         if (!composecommand)
396           found = false;
397       }
398       else if (opt == MUTT_MC_EDIT)
399       {
400         if (!editcommand)
401           found = false;
402       }
403       else if (opt == MUTT_MC_PRINT)
404       {
405         if (!printcommand)
406           found = false;
407       }
408 
409       if (!found)
410       {
411         /* reset */
412         if (entry)
413         {
414           FREE(&entry->command);
415           FREE(&entry->composecommand);
416           FREE(&entry->composetypecommand);
417           FREE(&entry->editcommand);
418           FREE(&entry->printcommand);
419           FREE(&entry->nametemplate);
420           FREE(&entry->convert);
421           entry->needsterminal = false;
422           entry->copiousoutput = false;
423           entry->xneomuttkeep = false;
424         }
425       }
426     } /* while (!found && (buf = mutt_file_read_line ())) */
427     mutt_file_fclose(&fp);
428   } /* if ((fp = fopen ())) */
429   FREE(&buf);
430   return found;
431 }
432 
433 /**
434  * mailcap_entry_new - Allocate memory for a new rfc1524 entry
435  * @retval ptr An un-initialized struct MailcapEntry
436  */
mailcap_entry_new(void)437 struct MailcapEntry *mailcap_entry_new(void)
438 {
439   return mutt_mem_calloc(1, sizeof(struct MailcapEntry));
440 }
441 
442 /**
443  * mailcap_entry_free - Deallocate an struct MailcapEntry
444  * @param[out] ptr MailcapEntry to deallocate
445  */
mailcap_entry_free(struct MailcapEntry ** ptr)446 void mailcap_entry_free(struct MailcapEntry **ptr)
447 {
448   if (!ptr || !*ptr)
449     return;
450 
451   struct MailcapEntry *me = *ptr;
452 
453   FREE(&me->command);
454   FREE(&me->testcommand);
455   FREE(&me->composecommand);
456   FREE(&me->composetypecommand);
457   FREE(&me->editcommand);
458   FREE(&me->printcommand);
459   FREE(&me->nametemplate);
460   FREE(ptr);
461 }
462 
463 /**
464  * mailcap_lookup - Find given type in the list of mailcap files
465  * @param a      Message body
466  * @param type   Text type in "type/subtype" format
467  * @param typelen Length of the type
468  * @param entry  struct MailcapEntry to populate with results
469  * @param opt    Type of mailcap entry to lookup, see #MailcapLookup
470  * @retval true  If *entry is not NULL it populates it with the mailcap entry
471  * @retval false No matching entry is found
472  *
473  * Find the given type in the list of mailcap files.
474  */
mailcap_lookup(struct Body * a,char * type,size_t typelen,struct MailcapEntry * entry,enum MailcapLookup opt)475 bool mailcap_lookup(struct Body *a, char *type, size_t typelen,
476                     struct MailcapEntry *entry, enum MailcapLookup opt)
477 {
478   /* rfc1524 specifies that a path of mailcap files should be searched.
479    * joy.  They say
480    * $HOME/.mailcap:/etc/mailcap:/usr/etc/mailcap:/usr/local/etc/mailcap, etc
481    * and overridden by the MAILCAPS environment variable, and, just to be nice,
482    * we'll make it specifiable in .neomuttrc */
483   const struct Slist *c_mailcap_path =
484       cs_subset_slist(NeoMutt->sub, "mailcap_path");
485   if (!c_mailcap_path || (c_mailcap_path->count == 0))
486   {
487     /* L10N:
488        Mutt is trying to look up a mailcap value, but $mailcap_path is empty.
489        We added a reference to the MAILCAPS environment variable as a hint too.
490 
491        Because the variable is automatically populated by Mutt, this
492        should only occur if the user deliberately runs in their shell:
493          export MAILCAPS=
494 
495        or deliberately runs inside Mutt or their .muttrc:
496          set mailcap_path=""
497          -or-
498          unset mailcap_path
499     */
500     mutt_error(_("Neither mailcap_path nor MAILCAPS specified"));
501     return false;
502   }
503 
504   mutt_check_lookup_list(a, type, typelen);
505 
506   struct Buffer *path = mutt_buffer_pool_get();
507   bool found = false;
508 
509   struct ListNode *np = NULL;
510   STAILQ_FOREACH(np, &c_mailcap_path->head, entries)
511   {
512     mutt_buffer_strcpy(path, np->data);
513     mutt_buffer_expand_path(path);
514 
515     mutt_debug(LL_DEBUG2, "Checking mailcap file: %s\n", mutt_buffer_string(path));
516     found = rfc1524_mailcap_parse(a, mutt_buffer_string(path), type, entry, opt);
517     if (found)
518       break;
519   }
520 
521   mutt_buffer_pool_release(&path);
522 
523   if (entry && !found)
524     mutt_error(_("mailcap entry for type %s not found"), type);
525 
526   return found;
527 }
528 
529 /**
530  * mailcap_expand_filename - Expand a new filename from a template or existing filename
531  * @param nametemplate Template
532  * @param oldfile      Original filename
533  * @param newfile      Buffer for new filename
534  *
535  * If there is no nametemplate, the stripped oldfile name is used as the
536  * template for newfile.
537  *
538  * If there is no oldfile, the stripped nametemplate name is used as the
539  * template for newfile.
540  *
541  * If both a nametemplate and oldfile are specified, the template is checked
542  * for a "%s". If none is found, the nametemplate is used as the template for
543  * newfile.  The first path component of the nametemplate and oldfile are ignored.
544  */
mailcap_expand_filename(const char * nametemplate,const char * oldfile,struct Buffer * newfile)545 void mailcap_expand_filename(const char *nametemplate, const char *oldfile,
546                              struct Buffer *newfile)
547 {
548   int i, j, k;
549   char *s = NULL;
550   bool lmatch = false, rmatch = false;
551 
552   mutt_buffer_reset(newfile);
553 
554   /* first, ignore leading path components */
555 
556   if (nametemplate && (s = strrchr(nametemplate, '/')))
557     nametemplate = s + 1;
558 
559   if (oldfile && (s = strrchr(oldfile, '/')))
560     oldfile = s + 1;
561 
562   if (!nametemplate)
563   {
564     if (oldfile)
565       mutt_buffer_strcpy(newfile, oldfile);
566   }
567   else if (!oldfile)
568   {
569     mutt_file_expand_fmt(newfile, nametemplate, "neomutt");
570   }
571   else /* oldfile && nametemplate */
572   {
573     /* first, compare everything left from the "%s"
574      * (if there is one).  */
575 
576     lmatch = true;
577     bool ps = false;
578     for (i = 0; nametemplate[i]; i++)
579     {
580       if ((nametemplate[i] == '%') && (nametemplate[i + 1] == 's'))
581       {
582         ps = true;
583         break;
584       }
585 
586       /* note that the following will _not_ read beyond oldfile's end. */
587 
588       if (lmatch && (nametemplate[i] != oldfile[i]))
589         lmatch = false;
590     }
591 
592     if (ps)
593     {
594       /* If we had a "%s", check the rest. */
595 
596       /* now, for the right part: compare everything right from
597        * the "%s" to the final part of oldfile.
598        *
599        * The logic here is as follows:
600        *
601        * - We start reading from the end.
602        * - There must be a match _right_ from the "%s",
603        *   thus the i + 2.
604        * - If there was a left hand match, this stuff
605        *   must not be counted again.  That's done by the
606        *   condition (j >= (lmatch ? i : 0)).  */
607 
608       rmatch = true;
609 
610       for (j = mutt_str_len(oldfile) - 1, k = mutt_str_len(nametemplate) - 1;
611            (j >= (lmatch ? i : 0)) && (k >= (i + 2)); j--, k--)
612       {
613         if (nametemplate[k] != oldfile[j])
614         {
615           rmatch = false;
616           break;
617         }
618       }
619 
620       /* Now, check if we had a full match. */
621 
622       if (k >= i + 2)
623         rmatch = false;
624 
625       struct Buffer *left = mutt_buffer_pool_get();
626       struct Buffer *right = mutt_buffer_pool_get();
627 
628       if (!lmatch)
629         mutt_buffer_strcpy_n(left, nametemplate, i);
630       if (!rmatch)
631         mutt_buffer_strcpy(right, nametemplate + i + 2);
632       mutt_buffer_printf(newfile, "%s%s%s", mutt_buffer_string(left), oldfile,
633                          mutt_buffer_string(right));
634 
635       mutt_buffer_pool_release(&left);
636       mutt_buffer_pool_release(&right);
637     }
638     else
639     {
640       /* no "%s" in the name template. */
641       mutt_buffer_strcpy(newfile, nametemplate);
642     }
643   }
644 
645   mutt_adv_mktemp(newfile);
646 }
647