• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 17366932035

31 Aug 2025 12:06PM UTC coverage: 50.099% (+0.05%) from 50.049%
17366932035

push

github

web-flow
tweak observer event types (#4023)

9132 of 18228 relevant lines covered (50.1%)

272.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.03
/email/parse.c
1
/**
2
 * @file
3
 * Miscellaneous email parsing routines
4
 *
5
 * @authors
6
 * Copyright (C) 2016-2023 Richard Russon <rich@flatcap.org>
7
 * Copyright (C) 2017-2023 Pietro Cerutti <gahr@gahr.ch>
8
 * Copyright (C) 2019 Federico Kircheis <federico.kircheis@gmail.com>
9
 * Copyright (C) 2019 Ian Zimmerman <itz@no-use.mooo.com>
10
 * Copyright (C) 2021 Christian Ludwig <ludwig@ma.tum.de>
11
 * Copyright (C) 2022 David Purton <dcpurton@marshwiggle.net>
12
 * Copyright (C) 2023 Steinar H Gunderson <steinar+neomutt@gunderson.no>
13
 *
14
 * @copyright
15
 * This program is free software: you can redistribute it and/or modify it under
16
 * the terms of the GNU General Public License as published by the Free Software
17
 * Foundation, either version 2 of the License, or (at your option) any later
18
 * version.
19
 *
20
 * This program is distributed in the hope that it will be useful, but WITHOUT
21
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
22
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
23
 * details.
24
 *
25
 * You should have received a copy of the GNU General Public License along with
26
 * this program.  If not, see <http://www.gnu.org/licenses/>.
27
 */
28

29
/**
30
 * @page email_parse Email parsing code
31
 *
32
 * Miscellaneous email parsing routines
33
 */
34

35
#include "config.h"
36
#include <errno.h>
37
#include <string.h>
38
#include <time.h>
39
#include "mutt/lib.h"
40
#include "address/lib.h"
41
#include "config/lib.h"
42
#include "core/lib.h"
43
#include "mutt.h"
44
#include "parse.h"
45
#include "body.h"
46
#include "email.h"
47
#include "envelope.h"
48
#include "from.h"
49
#include "globals.h"
50
#include "mime.h"
51
#include "parameter.h"
52
#include "rfc2047.h"
53
#include "rfc2231.h"
54
#include "url.h"
55
#ifdef USE_AUTOCRYPT
56
#include "autocrypt/lib.h"
57
#endif
58

59
/* If the 'Content-Length' is bigger than 1GiB, then it's clearly wrong.
60
 * Cap the value to prevent overflow of Body.length */
61
#define CONTENT_TOO_BIG (1 << 30)
62

63
static void parse_part(FILE *fp, struct Body *b, int *counter);
64
static struct Body *rfc822_parse_message(FILE *fp, struct Body *parent, int *counter);
65
static struct Body *parse_multipart(FILE *fp, const char *boundary,
66
                                    LOFF_T end_off, bool digest, int *counter);
67

68
/**
69
 * mutt_filter_commandline_header_tag - Sanitise characters in a header tag
70
 * @param header String to sanitise
71
 */
72
void mutt_filter_commandline_header_tag(char *header)
2✔
73
{
74
  if (!header)
2✔
75
    return;
76

77
  for (; (*header != '\0'); header++)
8✔
78
  {
79
    if ((*header < 33) || (*header > 126) || (*header == ':'))
6✔
80
      *header = '?';
×
81
  }
82
}
83

84
/**
85
 * mutt_filter_commandline_header_value - Sanitise characters in a header value
86
 * @param header String to sanitise
87
 *
88
 * It might be preferable to use mutt_filter_unprintable() instead.
89
 * This filter is being lax, but preventing a header injection via an embedded
90
 * newline.
91
 */
92
void mutt_filter_commandline_header_value(char *header)
1✔
93
{
94
  if (!header)
1✔
95
    return;
96

97
  for (; (*header != '\0'); header++)
24✔
98
  {
99
    if ((*header == '\n') || (*header == '\r'))
23✔
100
      *header = ' ';
×
101
  }
102
}
103

104
/**
105
 * mutt_auto_subscribe - Check if user is subscribed to mailing list
106
 * @param mailto URL of mailing list subscribe
107
 */
108
void mutt_auto_subscribe(const char *mailto)
1✔
109
{
110
  if (!mailto)
1✔
111
    return;
1✔
112

113
  if (!AutoSubscribeCache)
×
114
    AutoSubscribeCache = mutt_hash_new(200, MUTT_HASH_STRCASECMP | MUTT_HASH_STRDUP_KEYS);
×
115

116
  if (mutt_hash_find(AutoSubscribeCache, mailto))
×
117
    return;
118

119
  mutt_hash_insert(AutoSubscribeCache, mailto, AutoSubscribeCache);
×
120

121
  struct Envelope *lpenv = mutt_env_new(); /* parsed envelope from the List-Post mailto: URL */
×
122

123
  if (mutt_parse_mailto(lpenv, NULL, mailto) && !TAILQ_EMPTY(&lpenv->to))
×
124
  {
125
    const char *mailbox = buf_string(TAILQ_FIRST(&lpenv->to)->mailbox);
×
126
    if (mailbox && !mutt_regexlist_match(&SubscribedLists, mailbox) &&
×
127
        !mutt_regexlist_match(&UnMailLists, mailbox) &&
×
128
        !mutt_regexlist_match(&UnSubscribedLists, mailbox))
×
129
    {
130
      /* mutt_regexlist_add() detects duplicates, so it is safe to
131
       * try to add here without any checks. */
132
      mutt_regexlist_add(&MailLists, mailbox, REG_ICASE, NULL);
×
133
      mutt_regexlist_add(&SubscribedLists, mailbox, REG_ICASE, NULL);
×
134
    }
135
  }
136

137
  mutt_env_free(&lpenv);
×
138
}
139

140
/**
141
 * parse_parameters - Parse a list of Parameters
142
 * @param pl                 Parameter list for the results
143
 * @param s                  String to parse
144
 * @param allow_value_spaces Allow values with spaces
145
 *
146
 * Autocrypt defines an irregular parameter format that doesn't follow the
147
 * rfc.  It splits keydata across multiple lines without parameter continuations.
148
 * The allow_value_spaces parameter allows parsing those values which
149
 * are split by spaces when unfolded.
150
 */
151
static void parse_parameters(struct ParameterList *pl, const char *s, bool allow_value_spaces)
×
152
{
153
  struct Parameter *pnew = NULL;
154
  const char *p = NULL;
155
  size_t i;
156

157
  struct Buffer *buf = buf_pool_get();
×
158
  /* allow_value_spaces, especially with autocrypt keydata, can result
159
   * in quite large parameter values.  avoid frequent reallocs by
160
   * pre-sizing */
161
  if (allow_value_spaces)
×
162
    buf_alloc(buf, mutt_str_len(s));
×
163

164
  mutt_debug(LL_DEBUG2, "'%s'\n", s);
×
165

166
  const bool assumed = !slist_is_empty(cc_assumed_charset());
×
167
  while (*s)
×
168
  {
169
    buf_reset(buf);
×
170

171
    p = strpbrk(s, "=;");
×
172
    if (!p)
×
173
    {
174
      mutt_debug(LL_DEBUG1, "malformed parameter: %s\n", s);
×
175
      goto bail;
×
176
    }
177

178
    /* if we hit a ; now the parameter has no value, just skip it */
179
    if (*p != ';')
×
180
    {
181
      i = p - s;
×
182
      /* remove whitespace from the end of the attribute name */
183
      while ((i > 0) && mutt_str_is_email_wsp(s[i - 1]))
×
184
        i--;
185

186
      /* the check for the missing parameter token is here so that we can skip
187
       * over any quoted value that may be present.  */
188
      if (i == 0)
×
189
      {
190
        mutt_debug(LL_DEBUG1, "missing attribute: %s\n", s);
×
191
        pnew = NULL;
192
      }
193
      else
194
      {
195
        pnew = mutt_param_new();
×
196
        pnew->attribute = mutt_strn_dup(s, i);
×
197
      }
198

199
      do
200
      {
201
        s = mutt_str_skip_email_wsp(p + 1); /* skip over the =, or space if we loop */
×
202

203
        if (*s == '"')
×
204
        {
205
          bool state_ascii = true;
206
          s++;
×
207
          for (; *s; s++)
×
208
          {
209
            if (assumed)
×
210
            {
211
              // As iso-2022-* has a character of '"' with non-ascii state, ignore it
212
              if (*s == 0x1b)
×
213
              {
214
                if ((s[1] == '(') && ((s[2] == 'B') || (s[2] == 'J')))
×
215
                  state_ascii = true;
216
                else
217
                  state_ascii = false;
218
              }
219
            }
220
            if (state_ascii && (*s == '"'))
×
221
              break;
222
            if (*s == '\\')
×
223
            {
224
              if (s[1])
×
225
              {
226
                s++;
×
227
                /* Quote the next character */
228
                buf_addch(buf, *s);
×
229
              }
230
            }
231
            else
232
            {
233
              buf_addch(buf, *s);
×
234
            }
235
          }
236
          if (*s)
×
237
            s++; /* skip over the " */
×
238
        }
239
        else
240
        {
241
          for (; *s && *s != ' ' && *s != ';'; s++)
×
242
            buf_addch(buf, *s);
×
243
        }
244

245
        p = s;
246
      } while (allow_value_spaces && (*s == ' '));
×
247

248
      /* if the attribute token was missing, 'new' will be NULL */
249
      if (pnew)
×
250
      {
251
        pnew->value = buf_strdup(buf);
×
252

253
        mutt_debug(LL_DEBUG2, "parse_parameter: '%s' = '%s'\n",
×
254
                   pnew->attribute ? pnew->attribute : "", pnew->value ? pnew->value : "");
255

256
        /* Add this parameter to the list */
257
        TAILQ_INSERT_HEAD(pl, pnew, entries);
×
258
      }
259
    }
260
    else
261
    {
262
      mutt_debug(LL_DEBUG1, "parameter with no value: %s\n", s);
×
263
      s = p;
264
    }
265

266
    /* Find the next parameter */
267
    if ((*s != ';') && !(s = strchr(s, ';')))
×
268
      break; /* no more parameters */
269

270
    do
271
    {
272
      /* Move past any leading whitespace. the +1 skips over the semicolon */
273
      s = mutt_str_skip_email_wsp(s + 1);
×
274
    } while (*s == ';'); /* skip empty parameters */
×
275
  }
276

277
bail:
×
278

279
  rfc2231_decode_parameters(pl);
×
280
  buf_pool_release(&buf);
×
281
}
×
282

283
/**
284
 * parse_content_disposition - Parse a content disposition
285
 * @param s String to parse
286
 * @param b Body to save the result
287
 *
288
 * e.g. parse a string "inline" and set #DISP_INLINE.
289
 */
290
static void parse_content_disposition(const char *s, struct Body *b)
×
291
{
292
  struct ParameterList pl = TAILQ_HEAD_INITIALIZER(pl);
×
293

294
  if (mutt_istr_startswith(s, "inline"))
×
295
    b->disposition = DISP_INLINE;
×
296
  else if (mutt_istr_startswith(s, "form-data"))
×
297
    b->disposition = DISP_FORM_DATA;
×
298
  else
299
    b->disposition = DISP_ATTACH;
×
300

301
  /* Check to see if a default filename was given */
302
  s = strchr(s, ';');
×
303
  if (s)
×
304
  {
305
    s = mutt_str_skip_email_wsp(s + 1);
×
306
    parse_parameters(&pl, s, false);
×
307
    s = mutt_param_get(&pl, "filename");
×
308
    if (s)
×
309
      mutt_str_replace(&b->filename, s);
×
310
    s = mutt_param_get(&pl, "name");
×
311
    if (s)
×
312
      mutt_str_replace(&b->form_name, s);
×
313
    mutt_param_free(&pl);
×
314
  }
315
}
×
316

317
/**
318
 * parse_references - Parse references from an email header
319
 * @param head List to receive the references
320
 * @param s    String to parse
321
 */
322
static void parse_references(struct ListHead *head, const char *s)
×
323
{
324
  if (!head)
×
325
    return;
326

327
  char *m = NULL;
328
  for (size_t off = 0; (m = mutt_extract_message_id(s, &off)); s += off)
×
329
  {
330
    mutt_list_insert_head(head, m);
×
331
  }
332
}
333

334
/**
335
 * parse_content_language - Read the content's language
336
 * @param s Language string
337
 * @param b Body of the email
338
 */
339
static void parse_content_language(const char *s, struct Body *b)
×
340
{
341
  if (!s || !b)
×
342
    return;
343

344
  mutt_debug(LL_DEBUG2, "RFC8255 >> Content-Language set to %s\n", s);
×
345
  mutt_str_replace(&b->language, s);
×
346
}
347

348
/**
349
 * mutt_matches_ignore - Does the string match the ignore list
350
 * @param s String to check
351
 * @retval true String matches
352
 *
353
 * Checks Ignore and UnIgnore using mutt_list_match
354
 */
355
bool mutt_matches_ignore(const char *s)
1✔
356
{
357
  return mutt_list_match(s, &Ignore) && !mutt_list_match(s, &UnIgnore);
1✔
358
}
359

360
/**
361
 * mutt_check_mime_type - Check a MIME type string
362
 * @param s String to check
363
 * @retval enum ContentType, e.g. #TYPE_TEXT
364
 */
365
enum ContentType mutt_check_mime_type(const char *s)
1✔
366
{
367
  if (mutt_istr_equal("text", s))
1✔
368
    return TYPE_TEXT;
369
  if (mutt_istr_equal("multipart", s))
1✔
370
    return TYPE_MULTIPART;
371
  if (mutt_istr_equal("x-sun-attachment", s))
1✔
372
    return TYPE_MULTIPART;
373
  if (mutt_istr_equal("application", s))
1✔
374
    return TYPE_APPLICATION;
375
  if (mutt_istr_equal("message", s))
1✔
376
    return TYPE_MESSAGE;
377
  if (mutt_istr_equal("image", s))
1✔
378
    return TYPE_IMAGE;
379
  if (mutt_istr_equal("audio", s))
1✔
380
    return TYPE_AUDIO;
381
  if (mutt_istr_equal("video", s))
1✔
382
    return TYPE_VIDEO;
383
  if (mutt_istr_equal("model", s))
1✔
384
    return TYPE_MODEL;
385
  if (mutt_istr_equal("*", s))
1✔
386
    return TYPE_ANY;
387
  if (mutt_istr_equal(".*", s))
1✔
388
    return TYPE_ANY;
389

390
  return TYPE_OTHER;
391
}
392

393
/**
394
 * mutt_extract_message_id - Find a message-id
395
 * @param[in]  s String to parse
396
 * @param[out] len Number of bytes of s parsed
397
 * @retval ptr  Message id found
398
 * @retval NULL No more message ids
399
 */
400
char *mutt_extract_message_id(const char *s, size_t *len)
8✔
401
{
402
  if (!s || (*s == '\0'))
8✔
403
    return NULL;
404

405
  char *decoded = mutt_str_dup(s);
6✔
406
  rfc2047_decode(&decoded);
6✔
407

408
  char *res = NULL;
409

410
  for (const char *p = decoded, *beg = NULL; *p; p++)
92✔
411
  {
412
    if (*p == '<')
89✔
413
    {
414
      beg = p;
415
      continue;
4✔
416
    }
417

418
    if (beg && (*p == '>'))
85✔
419
    {
420
      if (len)
3✔
421
        *len = p - decoded + 1;
3✔
422
      res = mutt_strn_dup(beg, (p + 1) - beg);
3✔
423
      break;
3✔
424
    }
425
  }
426

427
  FREE(&decoded);
6✔
428
  return res;
6✔
429
}
430

431
/**
432
 * mutt_check_encoding - Check the encoding type
433
 * @param c String to check
434
 * @retval enum ContentEncoding, e.g. #ENC_QUOTED_PRINTABLE
435
 */
436
int mutt_check_encoding(const char *c)
1✔
437
{
438
  if (mutt_istr_startswith(c, "7bit"))
1✔
439
    return ENC_7BIT;
440
  if (mutt_istr_startswith(c, "8bit"))
1✔
441
    return ENC_8BIT;
442
  if (mutt_istr_startswith(c, "binary"))
1✔
443
    return ENC_BINARY;
444
  if (mutt_istr_startswith(c, "quoted-printable"))
1✔
445
    return ENC_QUOTED_PRINTABLE;
446
  if (mutt_istr_startswith(c, "base64"))
1✔
447
    return ENC_BASE64;
448
  if (mutt_istr_startswith(c, "x-uuencode"))
1✔
449
    return ENC_UUENCODED;
450
  if (mutt_istr_startswith(c, "uuencode"))
1✔
451
    return ENC_UUENCODED;
452
  return ENC_OTHER;
453
}
454

455
/**
456
 * mutt_parse_content_type - Parse a content type
457
 * @param s String to parse
458
 * @param b Body to save the result
459
 *
460
 * e.g. parse a string "inline" and set #DISP_INLINE.
461
 */
462
void mutt_parse_content_type(const char *s, struct Body *b)
2✔
463
{
464
  if (!s || !b)
2✔
465
    return;
466

467
  FREE(&b->subtype);
×
468
  mutt_param_free(&b->parameter);
×
469

470
  /* First extract any existing parameters */
471
  char *pc = strchr(s, ';');
×
472
  if (pc)
×
473
  {
474
    *pc++ = 0;
×
475
    while (*pc && mutt_isspace(*pc))
×
476
      pc++;
×
477
    parse_parameters(&b->parameter, pc, false);
×
478

479
    /* Some pre-RFC1521 gateways still use the "name=filename" convention,
480
     * but if a filename has already been set in the content-disposition,
481
     * let that take precedence, and don't set it here */
482
    pc = mutt_param_get(&b->parameter, "name");
×
483
    if (pc && !b->filename)
×
484
      b->filename = mutt_str_dup(pc);
×
485

486
    /* this is deep and utter perversion */
487
    pc = mutt_param_get(&b->parameter, "conversions");
×
488
    if (pc)
×
489
      b->encoding = mutt_check_encoding(pc);
×
490
  }
491

492
  /* Now get the subtype */
493
  char *subtype = strchr(s, '/');
×
494
  if (subtype)
×
495
  {
496
    *subtype++ = '\0';
×
497
    for (pc = subtype; *pc && !mutt_isspace(*pc) && (*pc != ';'); pc++)
×
498
      ; // do nothing
499

500
    *pc = '\0';
×
501
    mutt_str_replace(&b->subtype, subtype);
×
502
  }
503

504
  /* Finally, get the major type */
505
  b->type = mutt_check_mime_type(s);
×
506

507
  if (mutt_istr_equal("x-sun-attachment", s))
×
508
    mutt_str_replace(&b->subtype, "x-sun-attachment");
×
509

510
  if (b->type == TYPE_OTHER)
×
511
  {
512
    mutt_str_replace(&b->xtype, s);
×
513
  }
514

515
  if (!b->subtype)
×
516
  {
517
    /* Some older non-MIME mailers (i.e., mailtool, elm) have a content-type
518
     * field, so we can attempt to convert the type to Body here.  */
519
    if (b->type == TYPE_TEXT)
×
520
    {
521
      b->subtype = mutt_str_dup("plain");
×
522
    }
523
    else if (b->type == TYPE_AUDIO)
×
524
    {
525
      b->subtype = mutt_str_dup("basic");
×
526
    }
527
    else if (b->type == TYPE_MESSAGE)
×
528
    {
529
      b->subtype = mutt_str_dup("rfc822");
×
530
    }
531
    else if (b->type == TYPE_OTHER)
×
532
    {
533
      char buf[128] = { 0 };
×
534

535
      b->type = TYPE_APPLICATION;
×
536
      snprintf(buf, sizeof(buf), "x-%s", s);
537
      b->subtype = mutt_str_dup(buf);
×
538
    }
539
    else
540
    {
541
      b->subtype = mutt_str_dup("x-unknown");
×
542
    }
543
  }
544

545
  /* Default character set for text types. */
546
  if (b->type == TYPE_TEXT)
×
547
  {
548
    pc = mutt_param_get(&b->parameter, "charset");
×
549
    if (pc)
×
550
    {
551
      /* Microsoft Outlook seems to think it is necessary to repeat
552
       * charset=, strip it off not to confuse ourselves */
553
      if (mutt_istrn_equal(pc, "charset=", sizeof("charset=") - 1))
×
554
        mutt_param_set(&b->parameter, "charset", pc + (sizeof("charset=") - 1));
×
555
    }
556
    else
557
    {
558
      mutt_param_set(&b->parameter, "charset",
×
559
                     mutt_ch_get_default_charset(cc_assumed_charset()));
560
    }
561
  }
562
}
563

564
#ifdef USE_AUTOCRYPT
565
/**
566
 * parse_autocrypt - Parse an Autocrypt header line
567
 * @param head Autocrypt header to insert before
568
 * @param s    Header string to parse
569
 * @retval ptr New AutocryptHeader inserted before head
570
 */
571
static struct AutocryptHeader *parse_autocrypt(struct AutocryptHeader *head, const char *s)
×
572
{
573
  struct AutocryptHeader *autocrypt = mutt_autocrypthdr_new();
×
574
  autocrypt->next = head;
×
575

576
  struct ParameterList pl = TAILQ_HEAD_INITIALIZER(pl);
×
577
  parse_parameters(&pl, s, true);
×
578
  if (TAILQ_EMPTY(&pl))
×
579
  {
580
    autocrypt->invalid = true;
×
581
    goto cleanup;
×
582
  }
583

584
  struct Parameter *p = NULL;
585
  TAILQ_FOREACH(p, &pl, entries)
×
586
  {
587
    if (mutt_istr_equal(p->attribute, "addr"))
×
588
    {
589
      if (autocrypt->addr)
×
590
      {
591
        autocrypt->invalid = true;
×
592
        goto cleanup;
×
593
      }
594
      autocrypt->addr = p->value;
×
595
      p->value = NULL;
×
596
    }
597
    else if (mutt_istr_equal(p->attribute, "prefer-encrypt"))
×
598
    {
599
      if (mutt_istr_equal(p->value, "mutual"))
×
600
        autocrypt->prefer_encrypt = true;
×
601
    }
602
    else if (mutt_istr_equal(p->attribute, "keydata"))
×
603
    {
604
      if (autocrypt->keydata)
×
605
      {
606
        autocrypt->invalid = true;
×
607
        goto cleanup;
×
608
      }
609
      autocrypt->keydata = p->value;
×
610
      p->value = NULL;
×
611
    }
612
    else if (p->attribute && (p->attribute[0] != '_'))
×
613
    {
614
      autocrypt->invalid = true;
×
615
      goto cleanup;
×
616
    }
617
  }
618

619
  /* Checking the addr against From, and for multiple valid headers
620
   * occurs later, after all the headers are parsed. */
621
  if (!autocrypt->addr || !autocrypt->keydata)
×
622
    autocrypt->invalid = true;
×
623

624
cleanup:
×
625
  mutt_param_free(&pl);
×
626
  return autocrypt;
×
627
}
628
#endif
629

630
/**
631
 * rfc2369_first_mailto - Extract the first mailto: URL from a RFC2369 list
632
 * @param body Body of the header
633
 * @retval ptr First mailto: URL found, or NULL if none was found
634
 */
635
static char *rfc2369_first_mailto(const char *body)
×
636
{
637
  for (const char *beg = body, *end = NULL; beg; beg = strchr(end, ','))
×
638
  {
639
    beg = strchr(beg, '<');
×
640
    if (!beg)
×
641
    {
642
      break;
643
    }
644
    beg++;
×
645
    end = strchr(beg, '>');
×
646
    if (!end)
×
647
    {
648
      break;
649
    }
650

651
    char *mlist = mutt_strn_dup(beg, end - beg);
×
652
    if (url_check_scheme(mlist) == U_MAILTO)
×
653
    {
654
      return mlist;
×
655
    }
656
    FREE(&mlist);
×
657
  }
658
  return NULL;
659
}
660

661
/**
662
 * mutt_rfc822_parse_line - Parse an email header
663
 * @param env       Envelope of the email
664
 * @param e         Email
665
 * @param name      Header field name, e.g. 'to'
666
 * @param name_len  Must be equivalent to strlen(name)
667
 * @param body      Header field body, e.g. 'john@example.com'
668
 * @param user_hdrs If true, save into the Envelope's userhdrs
669
 * @param weed      If true, perform header weeding (filtering)
670
 * @param do_2047   If true, perform RFC2047 decoding of the field
671
 * @retval 1 The field is recognised
672
 * @retval 0 The field is not recognised
673
 *
674
 * Process a line from an email header.  Each line that is recognised is parsed
675
 * and the information put in the Envelope or Header.
676
 */
677
int mutt_rfc822_parse_line(struct Envelope *env, struct Email *e,
5✔
678
                           const char *name, size_t name_len, const char *body,
679
                           bool user_hdrs, bool weed, bool do_2047)
680
{
681
  if (!env || !name)
5✔
682
    return 0;
683

684
  bool matched = false;
685

686
  switch (name[0] | 0x20)
3✔
687
  {
688
    case 'a':
2✔
689
      if ((name_len == 13) && eqi12(name + 1, "pparently-to"))
2✔
690
      {
691
        mutt_addrlist_parse(&env->to, body);
×
692
        matched = true;
×
693
      }
694
      else if ((name_len == 15) && eqi14(name + 1, "pparently-from"))
2✔
695
      {
696
        mutt_addrlist_parse(&env->from, body);
×
697
        matched = true;
×
698
      }
699
#ifdef USE_AUTOCRYPT
700
      else if ((name_len == 9) && eqi8(name + 1, "utocrypt"))
2✔
701
      {
702
        const bool c_autocrypt = cs_subset_bool(NeoMutt->sub, "autocrypt");
×
703
        if (c_autocrypt)
×
704
        {
705
          env->autocrypt = parse_autocrypt(env->autocrypt, body);
×
706
          matched = true;
707
        }
708
      }
709
      else if ((name_len == 16) && eqi15(name + 1, "utocrypt-gossip"))
2✔
710
      {
711
        const bool c_autocrypt = cs_subset_bool(NeoMutt->sub, "autocrypt");
×
712
        if (c_autocrypt)
×
713
        {
714
          env->autocrypt_gossip = parse_autocrypt(env->autocrypt_gossip, body);
×
715
          matched = true;
716
        }
717
      }
718
#endif
719
      break;
720

721
    case 'b':
×
722
      if ((name_len == 3) && eqi2(name + 1, "cc"))
×
723
      {
724
        mutt_addrlist_parse(&env->bcc, body);
×
725
        matched = true;
726
      }
727
      break;
728

729
    case 'c':
1✔
730
      if ((name_len == 2) && eqi1(name + 1, "c"))
1✔
731
      {
732
        mutt_addrlist_parse(&env->cc, body);
1✔
733
        matched = true;
1✔
734
      }
735
      else
736
      {
737
        if ((name_len >= 12) && eqi8(name, "content-"))
×
738
        {
739
          if ((name_len == 12) && eqi4(name + 8, "type"))
×
740
          {
741
            if (e)
×
742
              mutt_parse_content_type(body, e->body);
×
743
            matched = true;
744
          }
745
          else if ((name_len == 16) && eqi8(name + 8, "language"))
×
746
          {
747
            if (e)
×
748
              parse_content_language(body, e->body);
×
749
            matched = true;
750
          }
751
          else if ((name_len == 25) && eqi17(name + 8, "transfer-encoding"))
×
752
          {
753
            if (e)
×
754
              e->body->encoding = mutt_check_encoding(body);
×
755
            matched = true;
756
          }
757
          else if ((name_len == 14) && eqi8(name + 6, "t-length"))
×
758
          {
759
            if (e)
×
760
            {
761
              unsigned long len = 0;
×
762
              e->body->length = mutt_str_atoul(body, &len) ? MIN(len, CONTENT_TOO_BIG) : -1;
×
763
            }
764
            matched = true;
765
          }
766
          else if ((name_len == 19) && eqi11(name + 8, "description"))
×
767
          {
768
            if (e)
×
769
            {
770
              mutt_str_replace(&e->body->description, body);
×
771
              rfc2047_decode(&e->body->description);
×
772
            }
773
            matched = true;
774
          }
775
          else if ((name_len == 19) && eqi11(name + 8, "disposition"))
×
776
          {
777
            if (e)
×
778
              parse_content_disposition(body, e->body);
×
779
            matched = true;
780
          }
781
        }
782
      }
783
      break;
784

785
    case 'd':
×
786
      if ((name_len != 4) || !eqi4(name, "date"))
×
787
        break;
788

789
      mutt_str_replace(&env->date, body);
×
790
      if (e)
×
791
      {
792
        struct Tz tz = { 0 };
×
793
        // the caller will check e->date_sent for -1
794
        e->date_sent = mutt_date_parse_date(body, &tz);
×
795
        if (e->date_sent > 0)
×
796
        {
797
          e->zhours = tz.zhours;
×
798
          e->zminutes = tz.zminutes;
×
799
          e->zoccident = tz.zoccident;
×
800
        }
801
      }
802
      matched = true;
803
      break;
804

805
    case 'e':
×
806
      if ((name_len == 7) && eqi6(name + 1, "xpires") && e)
×
807
      {
808
        const time_t expired = mutt_date_parse_date(body, NULL);
×
809
        if ((expired != -1) && (expired < mutt_date_now()))
×
810
        {
811
          e->expired = true;
×
812
        }
813
      }
814
      break;
815

816
    case 'f':
×
817
      if ((name_len == 4) && eqi4(name, "from"))
×
818
      {
819
        mutt_addrlist_parse(&env->from, body);
×
820
        matched = true;
×
821
      }
822
      else if ((name_len == 11) && eqi10(name + 1, "ollowup-to"))
×
823
      {
824
        if (!env->followup_to)
×
825
        {
826
          env->followup_to = mutt_str_dup(mutt_str_skip_whitespace(body));
×
827
          mutt_str_remove_trailing_ws(env->followup_to);
×
828
        }
829
        matched = true;
830
      }
831
      break;
832

833
    case 'i':
×
834
      if ((name_len != 11) || !eqi10(name + 1, "n-reply-to"))
×
835
        break;
836

837
      mutt_list_free(&env->in_reply_to);
×
838
      char *body2 = mutt_str_dup(body); // Create a mutable copy
×
839
      mutt_filter_commandline_header_value(body2);
×
840
      parse_references(&env->in_reply_to, body2);
×
841
      FREE(&body2);
×
842
      matched = true;
843
      break;
×
844

845
    case 'l':
×
846
      if ((name_len == 5) && eqi4(name + 1, "ines"))
×
847
      {
848
        if (e)
×
849
        {
850
          unsigned int ui = 0; // we don't want a negative number of lines
×
851
          mutt_str_atoui(body, &ui);
×
852
          e->lines = ui;
×
853
        }
854

855
        matched = true;
856
      }
857
      else if ((name_len == 9) && eqi8(name + 1, "ist-post"))
×
858
      {
859
        /* RFC2369 */
860
        if (!mutt_strn_equal(mutt_str_skip_whitespace(body), "NO", 2))
×
861
        {
862
          char *mailto = rfc2369_first_mailto(body);
×
863
          if (mailto)
×
864
          {
865
            FREE(&env->list_post);
×
866
            env->list_post = mailto;
×
867
            const bool c_auto_subscribe = cs_subset_bool(NeoMutt->sub, "auto_subscribe");
×
868
            if (c_auto_subscribe)
×
869
              mutt_auto_subscribe(env->list_post);
×
870
          }
871
        }
872
        matched = true;
873
      }
874
      else if ((name_len == 14) && eqi13(name + 1, "ist-subscribe"))
×
875
      {
876
        /* RFC2369 */
877
        char *mailto = rfc2369_first_mailto(body);
×
878
        if (mailto)
×
879
        {
880
          FREE(&env->list_subscribe);
×
881
          env->list_subscribe = mailto;
×
882
        }
883
        matched = true;
884
      }
885
      else if ((name_len == 16) && eqi15(name + 1, "ist-unsubscribe"))
×
886
      {
887
        /* RFC2369 */
888
        char *mailto = rfc2369_first_mailto(body);
×
889
        if (mailto)
×
890
        {
891
          FREE(&env->list_unsubscribe);
×
892
          env->list_unsubscribe = mailto;
×
893
        }
894
        matched = true;
895
      }
896
      break;
897

898
    case 'm':
×
899
      if ((name_len == 12) && eqi11(name + 1, "ime-version"))
×
900
      {
901
        if (e)
×
902
          e->mime = true;
×
903
        matched = true;
904
      }
905
      else if ((name_len == 10) && eqi9(name + 1, "essage-id"))
×
906
      {
907
        /* We add a new "Message-ID:" when building a message */
908
        FREE(&env->message_id);
×
909
        env->message_id = mutt_extract_message_id(body, NULL);
×
910
        matched = true;
×
911
      }
912
      else
913
      {
914
        if ((name_len >= 13) && eqi4(name + 1, "ail-"))
×
915
        {
916
          if ((name_len == 13) && eqi8(name + 5, "reply-to"))
×
917
          {
918
            /* override the Reply-To: field */
919
            mutt_addrlist_clear(&env->reply_to);
×
920
            mutt_addrlist_parse(&env->reply_to, body);
×
921
            matched = true;
×
922
          }
923
          else if ((name_len == 16) && eqi11(name + 5, "followup-to"))
×
924
          {
925
            mutt_addrlist_parse(&env->mail_followup_to, body);
×
926
            matched = true;
927
          }
928
        }
929
      }
930
      break;
931

932
    case 'n':
×
933
      if ((name_len == 10) && eqi9(name + 1, "ewsgroups"))
×
934
      {
935
        FREE(&env->newsgroups);
×
936
        env->newsgroups = mutt_str_dup(mutt_str_skip_whitespace(body));
×
937
        mutt_str_remove_trailing_ws(env->newsgroups);
×
938
        matched = true;
939
      }
940
      break;
941

942
    case 'o':
×
943
      /* field 'Organization:' saves only for pager! */
944
      if ((name_len == 12) && eqi11(name + 1, "rganization"))
×
945
      {
946
        if (!env->organization && !mutt_istr_equal(body, "unknown"))
×
947
          env->organization = mutt_str_dup(body);
×
948
      }
949
      break;
950

951
    case 'r':
×
952
      if ((name_len == 10) && eqi9(name + 1, "eferences"))
×
953
      {
954
        mutt_list_free(&env->references);
×
955
        parse_references(&env->references, body);
×
956
        matched = true;
×
957
      }
958
      else if ((name_len == 8) && eqi8(name, "reply-to"))
×
959
      {
960
        mutt_addrlist_parse(&env->reply_to, body);
×
961
        matched = true;
×
962
      }
963
      else if ((name_len == 11) && eqi10(name + 1, "eturn-path"))
×
964
      {
965
        mutt_addrlist_parse(&env->return_path, body);
×
966
        matched = true;
×
967
      }
968
      else if ((name_len == 8) && eqi8(name, "received"))
×
969
      {
970
        if (e && (e->received == 0))
×
971
        {
972
          char *d = strrchr(body, ';');
×
973
          if (d)
×
974
          {
975
            d = mutt_str_skip_email_wsp(d + 1);
×
976
            // the caller will check e->received for -1
977
            e->received = mutt_date_parse_date(d, NULL);
×
978
          }
979
        }
980
      }
981
      break;
982

983
    case 's':
×
984
      if ((name_len == 7) && eqi6(name + 1, "ubject"))
×
985
      {
986
        if (!env->subject)
×
987
          mutt_env_set_subject(env, body);
×
988
        matched = true;
989
      }
990
      else if ((name_len == 6) && eqi5(name + 1, "ender"))
×
991
      {
992
        mutt_addrlist_parse(&env->sender, body);
×
993
        matched = true;
×
994
      }
995
      else if ((name_len == 6) && eqi5(name + 1, "tatus"))
×
996
      {
997
        if (e)
×
998
        {
999
          while (*body)
×
1000
          {
1001
            switch (*body)
×
1002
            {
1003
              case 'O':
×
1004
              {
1005
                e->old = true;
×
1006
                break;
×
1007
              }
1008
              case 'R':
×
1009
                e->read = true;
×
1010
                break;
×
1011
              case 'r':
×
1012
                e->replied = true;
×
1013
                break;
×
1014
            }
1015
            body++;
×
1016
          }
1017
        }
1018
        matched = true;
1019
      }
1020
      else if (e && (name_len == 10) && eqi1(name + 1, "u") &&
×
1021
               (eqi8(name + 2, "persedes") || eqi8(name + 2, "percedes")))
×
1022
      {
1023
        FREE(&env->supersedes);
×
1024
        env->supersedes = mutt_str_dup(body);
×
1025
      }
1026
      break;
1027

1028
    case 't':
×
1029
      if ((name_len == 2) && eqi1(name + 1, "o"))
×
1030
      {
1031
        mutt_addrlist_parse(&env->to, body);
×
1032
        matched = true;
1033
      }
1034
      break;
1035

1036
    case 'x':
×
1037
      if ((name_len == 8) && eqi8(name, "x-status"))
×
1038
      {
1039
        if (e)
×
1040
        {
1041
          while (*body)
×
1042
          {
1043
            switch (*body)
×
1044
            {
1045
              case 'A':
×
1046
                e->replied = true;
×
1047
                break;
×
1048
              case 'D':
×
1049
                e->deleted = true;
×
1050
                break;
×
1051
              case 'F':
×
1052
                e->flagged = true;
×
1053
                break;
×
1054
              default:
1055
                break;
1056
            }
1057
            body++;
×
1058
          }
1059
        }
1060
        matched = true;
1061
      }
1062
      else if ((name_len == 7) && eqi6(name + 1, "-label"))
×
1063
      {
1064
        FREE(&env->x_label);
×
1065
        env->x_label = mutt_str_dup(body);
×
1066
        matched = true;
×
1067
      }
1068
      else if ((name_len == 12) && eqi11(name + 1, "-comment-to"))
×
1069
      {
1070
        if (!env->x_comment_to)
×
1071
          env->x_comment_to = mutt_str_dup(body);
×
1072
        matched = true;
1073
      }
1074
      else if ((name_len == 4) && eqi4(name, "xref"))
×
1075
      {
1076
        if (!env->xref)
×
1077
          env->xref = mutt_str_dup(body);
×
1078
        matched = true;
1079
      }
1080
      else if ((name_len == 13) && eqi12(name + 1, "-original-to"))
×
1081
      {
1082
        mutt_addrlist_parse(&env->x_original_to, body);
×
1083
        matched = true;
1084
      }
1085
      break;
1086

1087
    default:
1088
      break;
1089
  }
1090

1091
  /* Keep track of the user-defined headers */
1092
  if (!matched && user_hdrs)
3✔
1093
  {
1094
    const bool c_weed = cs_subset_bool(NeoMutt->sub, "weed");
×
1095
    char *dup = NULL;
×
1096
    mutt_str_asprintf(&dup, "%s: %s", name, body);
×
1097

1098
    if (!weed || !c_weed || !mutt_matches_ignore(dup))
×
1099
    {
1100
      struct ListNode *np = mutt_list_insert_tail(&env->userhdrs, dup);
×
1101
      if (do_2047)
×
1102
      {
1103
        rfc2047_decode(&np->data);
×
1104
      }
1105
    }
1106
    else
1107
    {
1108
      FREE(&dup);
×
1109
    }
1110
  }
1111

1112
  return matched;
3✔
1113
}
1114

1115
/**
1116
 * mutt_rfc822_read_line - Read a header line from a file
1117
 * @param fp      File to read from
1118
 * @param buf     Buffer to store the result
1119
 * @retval num Number of bytes read from fp
1120
 *
1121
 * Reads an arbitrarily long header field, and looks ahead for continuation
1122
 * lines.
1123
 */
1124
size_t mutt_rfc822_read_line(FILE *fp, struct Buffer *buf)
14✔
1125
{
1126
  if (!fp || !buf)
14✔
1127
    return 0;
1128

1129
  size_t read = 0;
1130
  char line[1024] = { 0 }; /* RFC2822 specifies a maximum line length of 998 */
12✔
1131

1132
  buf_reset(buf);
12✔
1133
  while (true)
1134
  {
7✔
1135
    if (!fgets(line, sizeof(line), fp))
19✔
1136
    {
1137
      return 0;
1138
    }
1139

1140
    const size_t linelen = mutt_str_len(line);
18✔
1141
    if (linelen == 0)
18✔
1142
    {
1143
      break;
1144
    }
1145

1146
    if (mutt_str_is_email_wsp(line[0]) && buf_is_empty(buf))
18✔
1147
    {
1148
      read = linelen;
1149
      break;
1150
    }
1151

1152
    read += linelen;
18✔
1153

1154
    size_t off = linelen - 1;
18✔
1155
    if (line[off] == '\n')
18✔
1156
    {
1157
      /* We did get a full line: remove trailing space */
1158
      do
1159
      {
1160
        line[off] = '\0';
25✔
1161
      } while (off && mutt_str_is_email_wsp(line[--off]));
25✔
1162

1163
      /* check to see if the next line is a continuation line */
1164
      int ch = fgetc(fp);
1165
      if ((ch != ' ') && (ch != '\t'))
16✔
1166
      {
1167
        /* next line is a separate header field or EOH */
1168
        ungetc(ch, fp);
11✔
1169
        buf_addstr(buf, line);
11✔
1170
        break;
11✔
1171
      }
1172
      read++;
5✔
1173

1174
      /* eat tabs and spaces from the beginning of the continuation line */
1175
      while (((ch = fgetc(fp)) == ' ') || (ch == '\t'))
16✔
1176
      {
1177
        read++;
6✔
1178
      }
1179

1180
      ungetc(ch, fp);
5✔
1181
      line[off + 1] = ' '; /* string is still terminated because we removed
5✔
1182
                              at least one whitespace char above */
1183
    }
1184

1185
    buf_addstr(buf, line);
7✔
1186
  }
1187

1188
  return read;
1189
}
1190

1191
/**
1192
 * mutt_rfc822_read_header - Parses an RFC822 header
1193
 * @param fp        Stream to read from
1194
 * @param e         Current Email (optional)
1195
 * @param user_hdrs If set, store user headers
1196
 *                  Used for recall-message and postpone modes
1197
 * @param weed      If this parameter is set and the user has activated the
1198
 *                  $weed option, honor the header weed list for user headers.
1199
 *                  Used for recall-message
1200
 * @retval ptr Newly allocated envelope structure
1201
 *
1202
 * Caller should free the Envelope using mutt_env_free().
1203
 */
1204
struct Envelope *mutt_rfc822_read_header(FILE *fp, struct Email *e, bool user_hdrs, bool weed)
2✔
1205
{
1206
  if (!fp)
2✔
1207
    return NULL;
1208

1209
  struct Envelope *env = mutt_env_new();
1✔
1210
  char *p = NULL;
1211
  LOFF_T loc = e ? e->offset : ftello(fp);
1✔
1212
  if (loc < 0)
1✔
1213
  {
1214
    mutt_debug(LL_DEBUG1, "ftello: %s (errno %d)\n", strerror(errno), errno);
×
1215
    loc = 0;
1216
  }
1217

1218
  struct Buffer *line = buf_pool_get();
1✔
1219

1220
  if (e)
1✔
1221
  {
1222
    if (!e->body)
×
1223
    {
1224
      e->body = mutt_body_new();
×
1225

1226
      /* set the defaults from RFC1521 */
1227
      e->body->type = TYPE_TEXT;
×
1228
      e->body->subtype = mutt_str_dup("plain");
×
1229
      e->body->encoding = ENC_7BIT;
×
1230
      e->body->length = -1;
×
1231

1232
      /* RFC2183 says this is arbitrary */
1233
      e->body->disposition = DISP_INLINE;
×
1234
    }
1235
  }
1236

1237
  while (true)
1238
  {
1239
    LOFF_T line_start_loc = loc;
1240
    size_t len = mutt_rfc822_read_line(fp, line);
1✔
1241
    if (buf_is_empty(line))
1✔
1242
    {
1243
      break;
1244
    }
1245
    loc += len;
×
1246
    const char *lines = buf_string(line);
×
1247
    p = strpbrk(lines, ": \t");
×
1248
    if (!p || (*p != ':'))
×
1249
    {
1250
      char return_path[1024] = { 0 };
×
1251
      time_t t = 0;
×
1252

1253
      /* some bogus MTAs will quote the original "From " line */
1254
      if (mutt_str_startswith(lines, ">From "))
×
1255
      {
1256
        continue; /* just ignore */
×
1257
      }
1258
      else if (is_from(lines, return_path, sizeof(return_path), &t))
×
1259
      {
1260
        /* MH sometimes has the From_ line in the middle of the header! */
1261
        if (e && (e->received == 0))
×
1262
          e->received = t - mutt_date_local_tz(t);
×
1263
        continue;
×
1264
      }
1265

1266
      /* We need to seek back to the start of the body. Note that we
1267
       * keep track of loc ourselves, since calling ftello() incurs
1268
       * a syscall, which can be expensive to do for every single line */
1269
      (void) mutt_file_seek(fp, line_start_loc, SEEK_SET);
×
1270
      break; /* end of header */
×
1271
    }
1272
    size_t name_len = p - lines;
×
1273

1274
    char buf[1024] = { 0 };
×
1275
    if (mutt_replacelist_match(&SpamList, buf, sizeof(buf), lines))
×
1276
    {
1277
      if (!mutt_regexlist_match(&NoSpamList, lines))
×
1278
      {
1279
        /* if spam tag already exists, figure out how to amend it */
1280
        if ((!buf_is_empty(&env->spam)) && (*buf != '\0'))
×
1281
        {
1282
          /* If `$spam_separator` defined, append with separator */
1283
          const char *const c_spam_separator = cs_subset_string(NeoMutt->sub, "spam_separator");
×
1284
          if (c_spam_separator)
×
1285
          {
1286
            buf_addstr(&env->spam, c_spam_separator);
×
1287
            buf_addstr(&env->spam, buf);
×
1288
          }
1289
          else /* overwrite */
1290
          {
1291
            buf_reset(&env->spam);
×
1292
            buf_addstr(&env->spam, buf);
×
1293
          }
1294
        }
1295
        else if (buf_is_empty(&env->spam) && (*buf != '\0'))
×
1296
        {
1297
          /* spam tag is new, and match expr is non-empty; copy */
1298
          buf_addstr(&env->spam, buf);
×
1299
        }
1300
        else if (buf_is_empty(&env->spam))
×
1301
        {
1302
          /* match expr is empty; plug in null string if no existing tag */
1303
          buf_addstr(&env->spam, "");
×
1304
        }
1305

1306
        if (!buf_is_empty(&env->spam))
×
1307
          mutt_debug(LL_DEBUG5, "spam = %s\n", env->spam.data);
×
1308
      }
1309
    }
1310

1311
    *p = '\0';
×
1312
    p = mutt_str_skip_email_wsp(p + 1);
×
1313
    if (*p == '\0')
×
1314
      continue; /* skip empty header fields */
×
1315

1316
    mutt_rfc822_parse_line(env, e, lines, name_len, p, user_hdrs, weed, true);
×
1317
  }
1318

1319
  buf_pool_release(&line);
1✔
1320

1321
  if (e)
1✔
1322
  {
1323
    e->body->hdr_offset = e->offset;
×
1324
    e->body->offset = ftello(fp);
×
1325

1326
    rfc2047_decode_envelope(env);
×
1327

1328
    if (e->received < 0)
×
1329
    {
1330
      mutt_debug(LL_DEBUG1, "resetting invalid received time to 0\n");
×
1331
      e->received = 0;
×
1332
    }
1333

1334
    /* check for missing or invalid date */
1335
    if (e->date_sent <= 0)
×
1336
    {
1337
      mutt_debug(LL_DEBUG1, "no date found, using received time from msg separator\n");
×
1338
      e->date_sent = e->received;
×
1339
    }
1340

1341
#ifdef USE_AUTOCRYPT
1342
    const bool c_autocrypt = cs_subset_bool(NeoMutt->sub, "autocrypt");
×
1343
    if (c_autocrypt)
×
1344
    {
1345
      mutt_autocrypt_process_autocrypt_header(e, env);
×
1346
      /* No sense in taking up memory after the header is processed */
1347
      mutt_autocrypthdr_free(&env->autocrypt);
×
1348
    }
1349
#endif
1350
  }
1351

1352
  return env;
1353
}
1354

1355
/**
1356
 * mutt_read_mime_header - Parse a MIME header
1357
 * @param fp      stream to read from
1358
 * @param digest  true if reading subparts of a multipart/digest
1359
 * @retval ptr New Body containing parsed structure
1360
 */
1361
struct Body *mutt_read_mime_header(FILE *fp, bool digest)
1✔
1362
{
1363
  if (!fp)
1✔
1364
    return NULL;
1365

1366
  struct Body *b = mutt_body_new();
×
1367
  struct Envelope *env = mutt_env_new();
×
1368
  char *c = NULL;
1369
  struct Buffer *buf = buf_pool_get();
×
1370
  bool matched = false;
1371

1372
  b->hdr_offset = ftello(fp);
×
1373

1374
  b->encoding = ENC_7BIT; /* default from RFC1521 */
×
1375
  b->type = digest ? TYPE_MESSAGE : TYPE_TEXT;
×
1376
  b->disposition = DISP_INLINE;
×
1377

1378
  while (mutt_rfc822_read_line(fp, buf) != 0)
×
1379
  {
1380
    const char *line = buf_string(buf);
×
1381
    /* Find the value of the current header */
1382
    c = strchr(line, ':');
×
1383
    if (c)
×
1384
    {
1385
      *c = '\0';
×
1386
      c = mutt_str_skip_email_wsp(c + 1);
×
1387
      if (*c == '\0')
×
1388
      {
1389
        mutt_debug(LL_DEBUG1, "skipping empty header field: %s\n", line);
×
1390
        continue;
×
1391
      }
1392
    }
1393
    else
1394
    {
1395
      mutt_debug(LL_DEBUG1, "bogus MIME header: %s\n", line);
×
1396
      break;
×
1397
    }
1398

1399
    size_t plen = mutt_istr_startswith(line, "content-");
×
1400
    if (plen != 0)
×
1401
    {
1402
      if (mutt_istr_equal("type", line + plen))
×
1403
      {
1404
        mutt_parse_content_type(c, b);
×
1405
      }
1406
      else if (mutt_istr_equal("language", line + plen))
×
1407
      {
1408
        parse_content_language(c, b);
×
1409
      }
1410
      else if (mutt_istr_equal("transfer-encoding", line + plen))
×
1411
      {
1412
        b->encoding = mutt_check_encoding(c);
×
1413
      }
1414
      else if (mutt_istr_equal("disposition", line + plen))
×
1415
      {
1416
        parse_content_disposition(c, b);
×
1417
      }
1418
      else if (mutt_istr_equal("description", line + plen))
×
1419
      {
1420
        mutt_str_replace(&b->description, c);
×
1421
        rfc2047_decode(&b->description);
×
1422
      }
1423
      else if (mutt_istr_equal("id", line + plen))
×
1424
      {
1425
        // strip <angle braces> from Content-ID: header
1426
        char *id = c;
1427
        int cid_len = mutt_str_len(c);
×
1428
        if (cid_len > 2)
×
1429
        {
1430
          if (id[0] == '<')
×
1431
          {
1432
            id++;
×
1433
            cid_len--;
×
1434
          }
1435
          if (id[cid_len - 1] == '>')
×
1436
            id[cid_len - 1] = '\0';
×
1437
        }
1438
        mutt_str_replace(&b->content_id, id);
×
1439
      }
1440
    }
1441
    else if ((plen = mutt_istr_startswith(line, "x-sun-")))
×
1442
    {
1443
      if (mutt_istr_equal("data-type", line + plen))
×
1444
      {
1445
        mutt_parse_content_type(c, b);
×
1446
      }
1447
      else if (mutt_istr_equal("encoding-info", line + plen))
×
1448
      {
1449
        b->encoding = mutt_check_encoding(c);
×
1450
      }
1451
      else if (mutt_istr_equal("content-lines", line + plen))
×
1452
      {
1453
        mutt_param_set(&b->parameter, "content-lines", c);
×
1454
      }
1455
      else if (mutt_istr_equal("data-description", line + plen))
×
1456
      {
1457
        mutt_str_replace(&b->description, c);
×
1458
        rfc2047_decode(&b->description);
×
1459
      }
1460
    }
1461
    else
1462
    {
1463
      if (mutt_rfc822_parse_line(env, NULL, line, strlen(line), c, false, false, false))
×
1464
      {
1465
        matched = true;
1466
      }
1467
    }
1468
  }
1469
  b->offset = ftello(fp); /* Mark the start of the real data */
×
1470
  if ((b->type == TYPE_TEXT) && !b->subtype)
×
1471
    b->subtype = mutt_str_dup("plain");
×
1472
  else if ((b->type == TYPE_MESSAGE) && !b->subtype)
×
1473
    b->subtype = mutt_str_dup("rfc822");
×
1474

1475
  buf_pool_release(&buf);
×
1476

1477
  if (matched)
×
1478
  {
1479
    b->mime_headers = env;
×
1480
    rfc2047_decode_envelope(b->mime_headers);
×
1481
  }
1482
  else
1483
  {
1484
    mutt_env_free(&env);
×
1485
  }
1486

1487
  return b;
1488
}
1489

1490
/**
1491
 * mutt_is_message_type - Determine if a mime type matches a message or not
1492
 * @param type    Message type enum value
1493
 * @param subtype Message subtype
1494
 * @retval true  Type is message/news or message/rfc822
1495
 * @retval false Otherwise
1496
 */
1497
bool mutt_is_message_type(int type, const char *subtype)
1✔
1498
{
1499
  if (type != TYPE_MESSAGE)
1✔
1500
    return false;
1501

1502
  subtype = NONULL(subtype);
×
1503
  return (mutt_istr_equal(subtype, "rfc822") ||
×
1504
          mutt_istr_equal(subtype, "news") || mutt_istr_equal(subtype, "global"));
×
1505
}
1506

1507
/**
1508
 * parse_part - Parse a MIME part
1509
 * @param fp      File to read from
1510
 * @param b       Body to store the results in
1511
 * @param counter Number of parts processed so far
1512
 */
1513
static void parse_part(FILE *fp, struct Body *b, int *counter)
2✔
1514
{
1515
  if (!fp || !b)
2✔
1516
    return;
1517

1518
  const char *bound = NULL;
1519
  static unsigned short recurse_level = 0;
1520

1521
  if (recurse_level >= MUTT_MIME_MAX_DEPTH)
×
1522
  {
1523
    mutt_debug(LL_DEBUG1, "recurse level too deep. giving up\n");
×
1524
    return;
×
1525
  }
1526
  recurse_level++;
×
1527

1528
  switch (b->type)
×
1529
  {
1530
    case TYPE_MULTIPART:
×
1531
      if (mutt_istr_equal(b->subtype, "x-sun-attachment"))
×
1532
        bound = "--------";
1533
      else
1534
        bound = mutt_param_get(&b->parameter, "boundary");
×
1535

1536
      if (!mutt_file_seek(fp, b->offset, SEEK_SET))
×
1537
      {
1538
        goto bail;
×
1539
      }
1540
      b->parts = parse_multipart(fp, bound, b->offset + b->length,
×
1541
                                 mutt_istr_equal("digest", b->subtype), counter);
×
1542
      break;
×
1543

1544
    case TYPE_MESSAGE:
×
1545
      if (!b->subtype)
×
1546
        break;
1547

1548
      if (!mutt_file_seek(fp, b->offset, SEEK_SET))
×
1549
      {
1550
        goto bail;
×
1551
      }
1552
      if (mutt_is_message_type(b->type, b->subtype))
×
1553
        b->parts = rfc822_parse_message(fp, b, counter);
×
1554
      else if (mutt_istr_equal(b->subtype, "external-body"))
×
1555
        b->parts = mutt_read_mime_header(fp, 0);
×
1556
      else
1557
        goto bail;
×
1558
      break;
1559

1560
    default:
×
1561
      goto bail;
×
1562
  }
1563

1564
  /* try to recover from parsing error */
1565
  if (!b->parts)
×
1566
  {
1567
    b->type = TYPE_TEXT;
×
1568
    mutt_str_replace(&b->subtype, "plain");
×
1569
  }
1570
bail:
×
1571
  recurse_level--;
×
1572
}
1573

1574
/**
1575
 * parse_multipart - Parse a multipart structure
1576
 * @param fp       Stream to read from
1577
 * @param boundary Body separator
1578
 * @param end_off  Length of the multipart body (used when the final
1579
 *                 boundary is missing to avoid reading too far)
1580
 * @param digest   true if reading a multipart/digest
1581
 * @param counter  Number of parts processed so far
1582
 * @retval ptr New Body containing parsed structure
1583
 */
1584
static struct Body *parse_multipart(FILE *fp, const char *boundary,
2✔
1585
                                    LOFF_T end_off, bool digest, int *counter)
1586
{
1587
  if (!fp)
2✔
1588
    return NULL;
1589

1590
  if (!boundary)
1✔
1591
  {
1592
    mutt_error(_("multipart message has no boundary parameter"));
1✔
1593
    return NULL;
1✔
1594
  }
1595

1596
  char buf[1024] = { 0 };
×
1597
  struct Body *head = NULL, *last = NULL, *new_body = NULL;
×
1598
  bool final = false; /* did we see the ending boundary? */
1599

1600
  const size_t blen = mutt_str_len(boundary);
×
1601
  while ((ftello(fp) < end_off) && fgets(buf, sizeof(buf), fp))
×
1602
  {
1603
    const size_t len = mutt_str_len(buf);
×
1604

1605
    const size_t crlf = ((len > 1) && (buf[len - 2] == '\r')) ? 1 : 0;
×
1606

1607
    if ((buf[0] == '-') && (buf[1] == '-') && mutt_str_startswith(buf + 2, boundary))
×
1608
    {
1609
      if (last)
×
1610
      {
1611
        last->length = ftello(fp) - last->offset - len - 1 - crlf;
×
1612
        if (last->parts && (last->parts->length == 0))
×
1613
          last->parts->length = ftello(fp) - last->parts->offset - len - 1 - crlf;
×
1614
        /* if the body is empty, we can end up with a -1 length */
1615
        if (last->length < 0)
×
1616
          last->length = 0;
×
1617
      }
1618

1619
      if (len > 0)
×
1620
      {
1621
        /* Remove any trailing whitespace, up to the length of the boundary */
1622
        for (size_t i = len - 1; mutt_isspace(buf[i]) && (i >= (blen + 2)); i--)
×
1623
          buf[i] = '\0';
×
1624
      }
1625

1626
      /* Check for the end boundary */
1627
      if (mutt_str_equal(buf + blen + 2, "--"))
×
1628
      {
1629
        final = true;
1630
        break; /* done parsing */
1631
      }
1632
      else if (buf[2 + blen] == '\0')
×
1633
      {
1634
        new_body = mutt_read_mime_header(fp, digest);
×
1635
        if (!new_body)
×
1636
          break;
1637

1638
        if (mutt_param_get(&new_body->parameter, "content-lines"))
×
1639
        {
1640
          int lines = 0;
×
1641
          mutt_str_atoi(mutt_param_get(&new_body->parameter, "content-lines"), &lines);
×
1642
          for (; lines > 0; lines--)
×
1643
            if ((ftello(fp) >= end_off) || !fgets(buf, sizeof(buf), fp))
×
1644
              break;
1645
        }
1646

1647
        /* Consistency checking - catch bad attachment end boundaries */
1648
        if (new_body->offset > end_off)
×
1649
        {
1650
          mutt_body_free(&new_body);
×
1651
          break;
×
1652
        }
1653
        if (head)
×
1654
        {
1655
          last->next = new_body;
×
1656
          last = new_body;
1657
        }
1658
        else
1659
        {
1660
          last = new_body;
1661
          head = new_body;
1662
        }
1663

1664
        /* It seems more intuitive to add the counter increment to
1665
         * parse_part(), but we want to stop the case where a multipart
1666
         * contains thousands of tiny parts before the memory and data
1667
         * structures are allocated.  */
1668
        if (++(*counter) >= MUTT_MIME_MAX_PARTS)
×
1669
          break;
1670
      }
1671
    }
1672
  }
1673

1674
  /* in case of missing end boundary, set the length to something reasonable */
1675
  if (last && (last->length == 0) && !final)
×
1676
    last->length = end_off - last->offset;
×
1677

1678
  /* parse recursive MIME parts */
1679
  for (last = head; last; last = last->next)
×
1680
    parse_part(fp, last, counter);
×
1681

1682
  return head;
1683
}
1684

1685
/**
1686
 * rfc822_parse_message - Parse a Message/RFC822 body
1687
 * @param fp      Stream to read from
1688
 * @param parent  Info about the message/rfc822 body part
1689
 * @param counter Number of parts processed so far
1690
 * @retval ptr New Body containing parsed message
1691
 *
1692
 * @note This assumes that 'parent->length' has been set!
1693
 */
1694
static struct Body *rfc822_parse_message(FILE *fp, struct Body *parent, int *counter)
2✔
1695
{
1696
  if (!fp || !parent)
2✔
1697
    return NULL;
1698

1699
  parent->email = email_new();
×
1700
  parent->email->offset = ftello(fp);
×
1701
  parent->email->env = mutt_rfc822_read_header(fp, parent->email, false, false);
×
1702
  struct Body *msg = parent->email->body;
×
1703

1704
  /* ignore the length given in the content-length since it could be wrong
1705
   * and we already have the info to calculate the correct length */
1706
  /* if (msg->length == -1) */
1707
  msg->length = parent->length - (msg->offset - parent->offset);
×
1708

1709
  /* if body of this message is empty, we can end up with a negative length */
1710
  if (msg->length < 0)
×
1711
    msg->length = 0;
×
1712

1713
  parse_part(fp, msg, counter);
×
1714
  return msg;
×
1715
}
1716

1717
/**
1718
 * mailto_header_allowed - Is the string in the list
1719
 * @param s String to match
1720
 * @param h Head of the List
1721
 * @retval true String matches a List item (or List contains "*")
1722
 *
1723
 * This is a very specific function.  It searches a List of strings looking for
1724
 * a match.  If the list contains a string "*", then it match any input string.
1725
 *
1726
 * This is similar to mutt_list_match(), except that it
1727
 * doesn't allow prefix matches.
1728
 *
1729
 * @note The case of the strings is ignored.
1730
 */
1731
static bool mailto_header_allowed(const char *s, struct ListHead *h)
2✔
1732
{
1733
  if (!h)
2✔
1734
    return false;
1735

1736
  struct ListNode *np = NULL;
1737
  STAILQ_FOREACH(np, h, entries)
3✔
1738
  {
1739
    if ((*np->data == '*') || mutt_istr_equal(s, np->data))
3✔
1740
      return true;
2✔
1741
  }
1742
  return false;
1743
}
1744

1745
/**
1746
 * mutt_parse_mailto - Parse a mailto:// url
1747
 * @param[in]  env  Envelope to fill
1748
 * @param[out] body Body to
1749
 * @param[in]  src  String to parse
1750
 * @retval true  Success
1751
 * @retval false Error
1752
 */
1753
bool mutt_parse_mailto(struct Envelope *env, char **body, const char *src)
4✔
1754
{
1755
  if (!env || !src)
4✔
1756
    return false;
1757

1758
  struct Url *url = url_parse(src);
2✔
1759
  if (!url)
2✔
1760
    return false;
1761

1762
  if (url->host)
1✔
1763
  {
1764
    /* this is not a path-only URL */
1765
    url_free(&url);
×
1766
    return false;
×
1767
  }
1768

1769
  mutt_addrlist_parse(&env->to, url->path);
1✔
1770

1771
  struct UrlQuery *np;
1772
  STAILQ_FOREACH(np, &url->query_strings, entries)
3✔
1773
  {
1774
    mutt_filter_commandline_header_tag(np->name);
2✔
1775
    const char *tag = np->name;
2✔
1776
    char *value = np->value;
2✔
1777
    /* Determine if this header field is on the allowed list.  Since NeoMutt
1778
     * interprets some header fields specially (such as
1779
     * "Attach: ~/.gnupg/secring.gpg"), care must be taken to ensure that
1780
     * only safe fields are allowed.
1781
     *
1782
     * RFC2368, "4. Unsafe headers"
1783
     * The user agent interpreting a mailto URL SHOULD choose not to create
1784
     * a message if any of the headers are considered dangerous; it may also
1785
     * choose to create a message with only a subset of the headers given in
1786
     * the URL.  */
1787
    if (mailto_header_allowed(tag, &MailToAllow))
2✔
1788
    {
1789
      if (mutt_istr_equal(tag, "body"))
2✔
1790
      {
1791
        if (body)
1✔
1792
          mutt_str_replace(body, value);
1✔
1793
      }
1794
      else
1795
      {
1796
        char *scratch = NULL;
1✔
1797
        size_t taglen = mutt_str_len(tag);
1✔
1798

1799
        mutt_filter_commandline_header_value(value);
1✔
1800
        mutt_str_asprintf(&scratch, "%s: %s", tag, value);
1✔
1801
        scratch[taglen] = 0; /* overwrite the colon as mutt_rfc822_parse_line expects */
1✔
1802
        value = mutt_str_skip_email_wsp(&scratch[taglen + 1]);
1✔
1803
        mutt_rfc822_parse_line(env, NULL, scratch, taglen, value, true, false, true);
1✔
1804
        FREE(&scratch);
1✔
1805
      }
1806
    }
1807
  }
1808

1809
  /* RFC2047 decode after the RFC822 parsing */
1810
  rfc2047_decode_envelope(env);
1✔
1811

1812
  url_free(&url);
1✔
1813
  return true;
1✔
1814
}
1815

1816
/**
1817
 * mutt_parse_part - Parse a MIME part
1818
 * @param fp File to read from
1819
 * @param b  Body to store the results in
1820
 */
1821
void mutt_parse_part(FILE *fp, struct Body *b)
2✔
1822
{
1823
  int counter = 0;
2✔
1824

1825
  parse_part(fp, b, &counter);
2✔
1826
}
2✔
1827

1828
/**
1829
 * mutt_rfc822_parse_message - Parse a Message/RFC822 body
1830
 * @param fp Stream to read from
1831
 * @param b  Info about the message/rfc822 body part
1832
 * @retval ptr New Body containing parsed message
1833
 *
1834
 * @note This assumes that 'b->length' has been set!
1835
 */
1836
struct Body *mutt_rfc822_parse_message(FILE *fp, struct Body *b)
2✔
1837
{
1838
  int counter = 0;
2✔
1839

1840
  return rfc822_parse_message(fp, b, &counter);
2✔
1841
}
1842

1843
/**
1844
 * mutt_parse_multipart - Parse a multipart structure
1845
 * @param fp       Stream to read from
1846
 * @param boundary Body separator
1847
 * @param end_off  Length of the multipart body (used when the final
1848
 *                 boundary is missing to avoid reading too far)
1849
 * @param digest   true if reading a multipart/digest
1850
 * @retval ptr New Body containing parsed structure
1851
 */
1852
struct Body *mutt_parse_multipart(FILE *fp, const char *boundary, LOFF_T end_off, bool digest)
2✔
1853
{
1854
  int counter = 0;
2✔
1855

1856
  return parse_multipart(fp, boundary, end_off, digest, &counter);
2✔
1857
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc