Coverage Report

Created: 2023-06-07 06:15

/src/neomutt/mailcap.c
Line
Count
Source (jump to first uncovered line)
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
 */
67
int mailcap_expand_command(struct Body *a, const char *filename,
68
                           const char *type, struct Buffer *command)
69
0
{
70
0
  int needspipe = true;
71
0
  struct Buffer *buf = buf_pool_get();
72
0
  struct Buffer *quoted = buf_pool_get();
73
0
  struct Buffer *param = NULL;
74
0
  struct Buffer *type2 = NULL;
75
76
0
  const bool c_mailcap_sanitize = cs_subset_bool(NeoMutt->sub, "mailcap_sanitize");
77
0
  const char *cptr = buf_string(command);
78
0
  while (*cptr)
79
0
  {
80
0
    if (*cptr == '\\')
81
0
    {
82
0
      cptr++;
83
0
      if (*cptr)
84
0
        buf_addch(buf, *cptr++);
85
0
    }
86
0
    else if (*cptr == '%')
87
0
    {
88
0
      cptr++;
89
0
      if (*cptr == '{')
90
0
      {
91
0
        const char *pvalue2 = NULL;
92
93
0
        if (param)
94
0
          buf_reset(param);
95
0
        else
96
0
          param = buf_pool_get();
97
98
        /* Copy parameter name into param buffer */
99
0
        cptr++;
100
0
        while (*cptr && (*cptr != '}'))
101
0
          buf_addch(param, *cptr++);
102
103
        /* In send mode, use the current charset, since the message hasn't
104
         * been converted yet.   If noconv is set, then we assume the
105
         * charset parameter has the correct value instead. */
106
0
        if (mutt_istr_equal(buf_string(param), "charset") && a->charset && !a->noconv)
107
0
          pvalue2 = a->charset;
108
0
        else
109
0
          pvalue2 = mutt_param_get(&a->parameter, buf_string(param));
110
111
        /* Now copy the parameter value into param buffer */
112
0
        if (c_mailcap_sanitize)
113
0
          buf_sanitize_filename(param, NONULL(pvalue2), false);
114
0
        else
115
0
          buf_strcpy(param, NONULL(pvalue2));
116
117
0
        buf_quote_filename(quoted, buf_string(param), true);
118
0
        buf_addstr(buf, buf_string(quoted));
119
0
      }
120
0
      else if ((*cptr == 's') && filename)
121
0
      {
122
0
        buf_quote_filename(quoted, filename, true);
123
0
        buf_addstr(buf, buf_string(quoted));
124
0
        needspipe = false;
125
0
      }
126
0
      else if (*cptr == 't')
127
0
      {
128
0
        if (!type2)
129
0
        {
130
0
          type2 = buf_pool_get();
131
0
          if (c_mailcap_sanitize)
132
0
            buf_sanitize_filename(type2, type, false);
133
0
          else
134
0
            buf_strcpy(type2, type);
135
0
        }
136
0
        buf_quote_filename(quoted, buf_string(type2), true);
137
0
        buf_addstr(buf, buf_string(quoted));
138
0
      }
139
140
0
      if (*cptr)
141
0
        cptr++;
142
0
    }
143
0
    else
144
0
    {
145
0
      buf_addch(buf, *cptr++);
146
0
    }
147
0
  }
148
0
  buf_copy(command, buf);
149
150
0
  buf_pool_release(&buf);
151
0
  buf_pool_release(&quoted);
152
0
  buf_pool_release(&param);
153
0
  buf_pool_release(&type2);
154
155
0
  return needspipe;
156
0
}
157
158
/**
159
 * get_field - NUL terminate a RFC1524 field
160
 * @param s String to alter
161
 * @retval ptr  Start of next field
162
 * @retval NULL Error
163
 */
164
static char *get_field(char *s)
165
0
{
166
0
  if (!s)
167
0
    return NULL;
168
169
0
  char *ch = NULL;
170
171
0
  while ((ch = strpbrk(s, ";\\")))
172
0
  {
173
0
    if (*ch == '\\')
174
0
    {
175
0
      s = ch + 1;
176
0
      if (*s)
177
0
        s++;
178
0
    }
179
0
    else
180
0
    {
181
0
      *ch = '\0';
182
0
      ch = mutt_str_skip_email_wsp(ch + 1);
183
0
      break;
184
0
    }
185
0
  }
186
0
  mutt_str_remove_trailing_ws(s);
187
0
  return ch;
188
0
}
189
190
/**
191
 * get_field_text - Get the matching text from a mailcap
192
 * @param field    String to parse
193
 * @param entry    Save the entry here
194
 * @param type     Type, e.g. "text/plain"
195
 * @param filename Mailcap filename
196
 * @param line     Mailcap line
197
 * @retval 1 Success
198
 * @retval 0 Failure
199
 */
200
static int get_field_text(char *field, char **entry, const char *type,
201
                          const char *filename, int line)
202
0
{
203
0
  field = mutt_str_skip_whitespace(field);
204
0
  if (*field == '=')
205
0
  {
206
0
    if (entry)
207
0
    {
208
0
      field++;
209
0
      field = mutt_str_skip_whitespace(field);
210
0
      mutt_str_replace(entry, field);
211
0
    }
212
0
    return 1;
213
0
  }
214
0
  else
215
0
  {
216
0
    mutt_error(_("Improperly formatted entry for type %s in \"%s\" line %d"),
217
0
               type, filename, line);
218
0
    return 0;
219
0
  }
220
0
}
221
222
/**
223
 * rfc1524_mailcap_parse - Parse a mailcap entry
224
 * @param a        Email Body
225
 * @param filename Filename
226
 * @param type     Type, e.g. "text/plain"
227
 * @param entry    Entry, e.g. "compose"
228
 * @param opt      Option, see #MailcapLookup
229
 * @retval true  Success
230
 * @retval false Failure
231
 */
232
static bool rfc1524_mailcap_parse(struct Body *a, const char *filename, const char *type,
233
                                  struct MailcapEntry *entry, enum MailcapLookup opt)
234
0
{
235
0
  char *buf = NULL;
236
0
  bool found = false;
237
0
  int line = 0;
238
239
  /* rfc1524 mailcap file is of the format:
240
   * base/type; command; extradefs
241
   * type can be * for matching all
242
   * base with no /type is an implicit wild
243
   * command contains a %s for the filename to pass, default to pipe on stdin
244
   * extradefs are of the form:
245
   *  def1="definition"; def2="define \;";
246
   * line wraps with a \ at the end of the line
247
   * # for comments */
248
249
  /* find length of basetype */
250
0
  char *ch = strchr(type, '/');
251
0
  if (!ch)
252
0
    return false;
253
0
  const int btlen = ch - type;
254
255
0
  FILE *fp = fopen(filename, "r");
256
0
  if (fp)
257
0
  {
258
0
    size_t buflen;
259
0
    while (!found && (buf = mutt_file_read_line(buf, &buflen, fp, &line, MUTT_RL_CONT)))
260
0
    {
261
      /* ignore comments */
262
0
      if (*buf == '#')
263
0
        continue;
264
0
      mutt_debug(LL_DEBUG2, "mailcap entry: %s\n", buf);
265
266
      /* check type */
267
0
      ch = get_field(buf);
268
0
      if (!mutt_istr_equal(buf, type) && (!mutt_istrn_equal(buf, type, btlen) ||
269
0
                                          ((buf[btlen] != '\0') && /* implicit wild */
270
0
                                           !mutt_str_equal(buf + btlen, "/*")))) /* wildsubtype */
271
0
      {
272
0
        continue;
273
0
      }
274
275
      /* next field is the viewcommand */
276
0
      char *field = ch;
277
0
      ch = get_field(ch);
278
0
      if (entry)
279
0
        entry->command = mutt_str_dup(field);
280
281
      /* parse the optional fields */
282
0
      found = true;
283
0
      bool copiousoutput = false;
284
0
      bool composecommand = false;
285
0
      bool editcommand = false;
286
0
      bool printcommand = false;
287
288
0
      while (ch)
289
0
      {
290
0
        field = ch;
291
0
        ch = get_field(ch);
292
0
        mutt_debug(LL_DEBUG2, "field: %s\n", field);
293
0
        size_t plen;
294
295
0
        if (mutt_istr_equal(field, "needsterminal"))
296
0
        {
297
0
          if (entry)
298
0
            entry->needsterminal = true;
299
0
        }
300
0
        else if (mutt_istr_equal(field, "copiousoutput"))
301
0
        {
302
0
          copiousoutput = true;
303
0
          if (entry)
304
0
            entry->copiousoutput = true;
305
0
        }
306
0
        else if ((plen = mutt_istr_startswith(field, "composetyped")))
307
0
        {
308
          /* this compare most occur before compose to match correctly */
309
0
          if (get_field_text(field + plen, entry ? &entry->composetypecommand : NULL,
310
0
                             type, filename, line))
311
0
          {
312
0
            composecommand = true;
313
0
          }
314
0
        }
315
0
        else if ((plen = mutt_istr_startswith(field, "compose")))
316
0
        {
317
0
          if (get_field_text(field + plen, entry ? &entry->composecommand : NULL,
318
0
                             type, filename, line))
319
0
          {
320
0
            composecommand = true;
321
0
          }
322
0
        }
323
0
        else if ((plen = mutt_istr_startswith(field, "print")))
324
0
        {
325
0
          if (get_field_text(field + plen, entry ? &entry->printcommand : NULL,
326
0
                             type, filename, line))
327
0
          {
328
0
            printcommand = true;
329
0
          }
330
0
        }
331
0
        else if ((plen = mutt_istr_startswith(field, "edit")))
332
0
        {
333
0
          if (get_field_text(field + plen, entry ? &entry->editcommand : NULL,
334
0
                             type, filename, line))
335
0
          {
336
0
            editcommand = true;
337
0
          }
338
0
        }
339
0
        else if ((plen = mutt_istr_startswith(field, "nametemplate")))
340
0
        {
341
0
          get_field_text(field + plen, entry ? &entry->nametemplate : NULL,
342
0
                         type, filename, line);
343
0
        }
344
0
        else if ((plen = mutt_istr_startswith(field, "x-convert")))
345
0
        {
346
0
          get_field_text(field + plen, entry ? &entry->convert : NULL, type, filename, line);
347
0
        }
348
0
        else if ((plen = mutt_istr_startswith(field, "test")))
349
0
        {
350
          /* This routine executes the given test command to determine
351
           * if this is the right entry.  */
352
0
          char *test_command = NULL;
353
354
0
          if (get_field_text(field + plen, &test_command, type, filename, line) && test_command)
355
0
          {
356
0
            struct Buffer *command = buf_pool_get();
357
0
            struct Buffer *afilename = buf_pool_get();
358
0
            buf_strcpy(command, test_command);
359
0
            const bool c_mailcap_sanitize = cs_subset_bool(NeoMutt->sub, "mailcap_sanitize");
360
0
            if (c_mailcap_sanitize)
361
0
              buf_sanitize_filename(afilename, NONULL(a->filename), true);
362
0
            else
363
0
              buf_strcpy(afilename, NONULL(a->filename));
364
0
            mailcap_expand_command(a, buf_string(afilename), type, command);
365
0
            if (mutt_system(buf_string(command)))
366
0
            {
367
              /* a non-zero exit code means test failed */
368
0
              found = false;
369
0
            }
370
0
            FREE(&test_command);
371
0
            buf_pool_release(&command);
372
0
            buf_pool_release(&afilename);
373
0
          }
374
0
        }
375
0
        else if (mutt_istr_startswith(field, "x-neomutt-keep"))
376
0
        {
377
0
          if (entry)
378
0
            entry->xneomuttkeep = true;
379
0
        }
380
0
        else if (mutt_istr_startswith(field, "x-neomutt-nowrap"))
381
0
        {
382
0
          if (entry)
383
0
            entry->xneomuttnowrap = true;
384
0
          a->nowrap = true;
385
0
        }
386
0
      } /* while (ch) */
387
388
0
      if (opt == MUTT_MC_AUTOVIEW)
389
0
      {
390
0
        if (!copiousoutput)
391
0
          found = false;
392
0
      }
393
0
      else if (opt == MUTT_MC_COMPOSE)
394
0
      {
395
0
        if (!composecommand)
396
0
          found = false;
397
0
      }
398
0
      else if (opt == MUTT_MC_EDIT)
399
0
      {
400
0
        if (!editcommand)
401
0
          found = false;
402
0
      }
403
0
      else if (opt == MUTT_MC_PRINT)
404
0
      {
405
0
        if (!printcommand)
406
0
          found = false;
407
0
      }
408
409
0
      if (!found)
410
0
      {
411
        /* reset */
412
0
        if (entry)
413
0
        {
414
0
          FREE(&entry->command);
415
0
          FREE(&entry->composecommand);
416
0
          FREE(&entry->composetypecommand);
417
0
          FREE(&entry->editcommand);
418
0
          FREE(&entry->printcommand);
419
0
          FREE(&entry->nametemplate);
420
0
          FREE(&entry->convert);
421
0
          entry->needsterminal = false;
422
0
          entry->copiousoutput = false;
423
0
          entry->xneomuttkeep = false;
424
0
        }
425
0
      }
426
0
    } /* while (!found && (buf = mutt_file_read_line ())) */
427
0
    mutt_file_fclose(&fp);
428
0
  } /* if ((fp = fopen ())) */
429
0
  FREE(&buf);
430
0
  return found;
431
0
}
432
433
/**
434
 * mailcap_entry_new - Allocate memory for a new rfc1524 entry
435
 * @retval ptr An un-initialized struct MailcapEntry
436
 */
437
struct MailcapEntry *mailcap_entry_new(void)
438
0
{
439
0
  return mutt_mem_calloc(1, sizeof(struct MailcapEntry));
440
0
}
441
442
/**
443
 * mailcap_entry_free - Deallocate an struct MailcapEntry
444
 * @param[out] ptr MailcapEntry to deallocate
445
 */
446
void mailcap_entry_free(struct MailcapEntry **ptr)
447
0
{
448
0
  if (!ptr || !*ptr)
449
0
    return;
450
451
0
  struct MailcapEntry *me = *ptr;
452
453
0
  FREE(&me->command);
454
0
  FREE(&me->testcommand);
455
0
  FREE(&me->composecommand);
456
0
  FREE(&me->composetypecommand);
457
0
  FREE(&me->editcommand);
458
0
  FREE(&me->printcommand);
459
0
  FREE(&me->nametemplate);
460
0
  FREE(ptr);
461
0
}
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
 */
475
bool mailcap_lookup(struct Body *a, char *type, size_t typelen,
476
                    struct MailcapEntry *entry, enum MailcapLookup opt)
477
0
{
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
0
  const struct Slist *c_mailcap_path = cs_subset_slist(NeoMutt->sub, "mailcap_path");
484
0
  if (!c_mailcap_path || (c_mailcap_path->count == 0))
485
0
  {
486
    /* L10N:
487
       Mutt is trying to look up a mailcap value, but $mailcap_path is empty.
488
       We added a reference to the MAILCAPS environment variable as a hint too.
489
490
       Because the variable is automatically populated by Mutt, this
491
       should only occur if the user deliberately runs in their shell:
492
         export MAILCAPS=
493
494
       or deliberately runs inside Mutt or their .muttrc:
495
         set mailcap_path=""
496
         -or-
497
         unset mailcap_path
498
    */
499
0
    mutt_error(_("Neither mailcap_path nor MAILCAPS specified"));
500
0
    return false;
501
0
  }
502
503
0
  mutt_check_lookup_list(a, type, typelen);
504
505
0
  struct Buffer *path = buf_pool_get();
506
0
  bool found = false;
507
508
0
  struct ListNode *np = NULL;
509
0
  STAILQ_FOREACH(np, &c_mailcap_path->head, entries)
510
0
  {
511
0
    buf_strcpy(path, np->data);
512
0
    buf_expand_path(path);
513
514
0
    mutt_debug(LL_DEBUG2, "Checking mailcap file: %s\n", buf_string(path));
515
0
    found = rfc1524_mailcap_parse(a, buf_string(path), type, entry, opt);
516
0
    if (found)
517
0
      break;
518
0
  }
519
520
0
  buf_pool_release(&path);
521
522
0
  if (entry && !found)
523
0
    mutt_error(_("mailcap entry for type %s not found"), type);
524
525
0
  return found;
526
0
}
527
528
/**
529
 * mailcap_expand_filename - Expand a new filename from a template or existing filename
530
 * @param nametemplate Template
531
 * @param oldfile      Original filename
532
 * @param newfile      Buffer for new filename
533
 *
534
 * If there is no nametemplate, the stripped oldfile name is used as the
535
 * template for newfile.
536
 *
537
 * If there is no oldfile, the stripped nametemplate name is used as the
538
 * template for newfile.
539
 *
540
 * If both a nametemplate and oldfile are specified, the template is checked
541
 * for a "%s". If none is found, the nametemplate is used as the template for
542
 * newfile.  The first path component of the nametemplate and oldfile are ignored.
543
 */
544
void mailcap_expand_filename(const char *nametemplate, const char *oldfile,
545
                             struct Buffer *newfile)
546
0
{
547
0
  int i, j, k;
548
0
  char *s = NULL;
549
0
  bool lmatch = false, rmatch = false;
550
551
0
  buf_reset(newfile);
552
553
  /* first, ignore leading path components */
554
555
0
  if (nametemplate && (s = strrchr(nametemplate, '/')))
556
0
    nametemplate = s + 1;
557
558
0
  if (oldfile && (s = strrchr(oldfile, '/')))
559
0
    oldfile = s + 1;
560
561
0
  if (!nametemplate)
562
0
  {
563
0
    if (oldfile)
564
0
      buf_strcpy(newfile, oldfile);
565
0
  }
566
0
  else if (!oldfile)
567
0
  {
568
0
    mutt_file_expand_fmt(newfile, nametemplate, "neomutt");
569
0
  }
570
0
  else /* oldfile && nametemplate */
571
0
  {
572
    /* first, compare everything left from the "%s"
573
     * (if there is one).  */
574
575
0
    lmatch = true;
576
0
    bool ps = false;
577
0
    for (i = 0; nametemplate[i]; i++)
578
0
    {
579
0
      if ((nametemplate[i] == '%') && (nametemplate[i + 1] == 's'))
580
0
      {
581
0
        ps = true;
582
0
        break;
583
0
      }
584
585
      /* note that the following will _not_ read beyond oldfile's end. */
586
587
0
      if (lmatch && (nametemplate[i] != oldfile[i]))
588
0
        lmatch = false;
589
0
    }
590
591
0
    if (ps)
592
0
    {
593
      /* If we had a "%s", check the rest. */
594
595
      /* now, for the right part: compare everything right from
596
       * the "%s" to the final part of oldfile.
597
       *
598
       * The logic here is as follows:
599
       *
600
       * - We start reading from the end.
601
       * - There must be a match _right_ from the "%s",
602
       *   thus the i + 2.
603
       * - If there was a left hand match, this stuff
604
       *   must not be counted again.  That's done by the
605
       *   condition (j >= (lmatch ? i : 0)).  */
606
607
0
      rmatch = true;
608
609
0
      for (j = mutt_str_len(oldfile) - 1, k = mutt_str_len(nametemplate) - 1;
610
0
           (j >= (lmatch ? i : 0)) && (k >= (i + 2)); j--, k--)
611
0
      {
612
0
        if (nametemplate[k] != oldfile[j])
613
0
        {
614
0
          rmatch = false;
615
0
          break;
616
0
        }
617
0
      }
618
619
      /* Now, check if we had a full match. */
620
621
0
      if (k >= i + 2)
622
0
        rmatch = false;
623
624
0
      struct Buffer *left = buf_pool_get();
625
0
      struct Buffer *right = buf_pool_get();
626
627
0
      if (!lmatch)
628
0
        buf_strcpy_n(left, nametemplate, i);
629
0
      if (!rmatch)
630
0
        buf_strcpy(right, nametemplate + i + 2);
631
0
      buf_printf(newfile, "%s%s%s", buf_string(left), oldfile, buf_string(right));
632
633
0
      buf_pool_release(&left);
634
0
      buf_pool_release(&right);
635
0
    }
636
0
    else
637
0
    {
638
      /* no "%s" in the name template. */
639
0
      buf_strcpy(newfile, nametemplate);
640
0
    }
641
0
  }
642
643
0
  mutt_adv_mktemp(newfile);
644
0
}