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("ed);
151 mutt_buffer_pool_release(¶m);
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