Coverage Report

Created: 2026-10-17 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-mail/rfc822-parser.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "str.h"
5
#include "strescape.h"
6
#include "idna.h"
7
#include "idna-punycode.h"
8
#include "rfc822-parser.h"
9
10
/*
11
   atext        =       ALPHA / DIGIT / ; Any character except controls,
12
      "!" / "#" /     ;  SP, and specials.
13
      "$" / "%" /     ;  Used for atoms
14
      "&" / "'" /
15
      "*" / "+" /
16
      "-" / "/" /
17
      "=" / "?" /
18
      "^" / "_" /
19
      "`" / "{" /
20
      "|" / "}" /
21
      "~"
22
23
  MIME:
24
25
  token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
26
        or tspecials>
27
  tspecials :=  "(" / ")" / "<" / ">" / "@" /
28
    "," / ";" / ":" / "\" / <">
29
    "/" / "[" / "]" / "?" / "="
30
31
  So token is same as dot-atom, except stops also at '/', '?' and '='.
32
*/
33
34
/* atext chars are marked with 1, alpha and digits with 2,
35
   atext-but-mime-tspecials with 4 */
36
unsigned char rfc822_atext_chars[256] = {
37
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0-15 */
38
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16-31 */
39
  0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 4, /* 32-47 */
40
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 4, 0, 4, /* 48-63 */
41
  0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 64-79 */
42
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, /* 80-95 */
43
  1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 96-111 */
44
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, /* 112-127 */
45
46
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
47
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
48
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
49
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
50
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
51
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
52
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
53
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
54
};
55
56
void rfc822_parser_init(struct rfc822_parser_context *ctx,
57
      const unsigned char *data, size_t size,
58
      string_t *last_comment)
59
48.9k
{
60
48.9k
  i_zero(ctx);
61
48.9k
  ctx->data = data;
62
48.9k
  ctx->end = data + size;
63
48.9k
  ctx->last_comment = last_comment;
64
48.9k
}
65
66
int rfc822_skip_comment(struct rfc822_parser_context *ctx)
67
10.3k
{
68
10.3k
  const unsigned char *start;
69
10.3k
  size_t len;
70
10.3k
  int level = 1;
71
72
10.3k
  i_assert(*ctx->data == '(');
73
74
10.3k
  if (ctx->last_comment != NULL)
75
0
    str_truncate(ctx->last_comment, 0);
76
77
10.3k
  start = ++ctx->data;
78
1.66M
  for (; ctx->data < ctx->end; ctx->data++) {
79
1.65M
    switch (*ctx->data) {
80
136k
    case '\0':
81
136k
      if (ctx->last_comment != NULL &&
82
0
          ctx->nul_replacement_str != NULL) {
83
0
        str_append_data(ctx->last_comment, start,
84
0
            ctx->data - start);
85
0
        str_append(ctx->last_comment,
86
0
             ctx->nul_replacement_str);
87
0
        start = ctx->data + 1;
88
0
      }
89
136k
      break;
90
1.07M
    case '(':
91
1.07M
      level++;
92
1.07M
      break;
93
24.0k
    case ')':
94
24.0k
      if (--level == 0) {
95
2.57k
        if (ctx->last_comment != NULL) {
96
0
          str_append_data(ctx->last_comment, start,
97
0
              ctx->data - start);
98
0
        }
99
2.57k
        ctx->data++;
100
2.57k
        return ctx->data < ctx->end ? 1 : 0;
101
2.57k
      }
102
21.4k
      break;
103
21.4k
    case '\n':
104
      /* folding whitespace, remove the (CR)LF */
105
1.42k
      if (ctx->last_comment == NULL)
106
1.42k
        break;
107
0
      len = ctx->data - start;
108
0
      if (len > 0 && start[len-1] == '\r')
109
0
        len--;
110
0
      str_append_data(ctx->last_comment, start, len);
111
0
      start = ctx->data + 1;
112
0
      break;
113
1.98k
    case '\\':
114
1.98k
      ctx->data++;
115
1.98k
      if (ctx->data >= ctx->end)
116
452
        return -1;
117
118
1.53k
      if (*ctx->data == '\r' || *ctx->data == '\n' ||
119
1.06k
          *ctx->data == '\0') {
120
        /* quoted-pair doesn't allow CR/LF/NUL.
121
           They are part of the obs-qp though, so don't
122
           return them as error. */
123
827
        ctx->data--;
124
827
        break;
125
827
      }
126
707
      if (ctx->last_comment != NULL) {
127
0
        str_append_data(ctx->last_comment, start,
128
0
            ctx->data - start - 1);
129
0
      }
130
707
      start = ctx->data;
131
707
      break;
132
1.65M
    }
133
1.65M
  }
134
135
  /* missing ')' */
136
7.27k
  return -1;
137
10.3k
}
138
139
int rfc822_skip_lwsp(struct rfc822_parser_context *ctx)
140
974k
{
141
1.02M
  for (; ctx->data < ctx->end;) {
142
1.00M
    if (*ctx->data == ' ' || *ctx->data == '\t' ||
143
982k
        *ctx->data == '\r' || *ctx->data == '\n') {
144
49.4k
      ctx->data++;
145
49.4k
      continue;
146
49.4k
    }
147
148
952k
    if (*ctx->data != '(')
149
942k
      break;
150
151
10.3k
    if (rfc822_skip_comment(ctx) < 0)
152
7.73k
      return -1;
153
10.3k
  }
154
966k
  return ctx->data < ctx->end ? 1 : 0;
155
974k
}
156
157
int rfc822_parse_atom(struct rfc822_parser_context *ctx, string_t *str)
158
0
{
159
0
  const unsigned char *start;
160
161
  /*
162
     atom            = [CFWS] 1*atext [CFWS]
163
     atext           =
164
       ; Any character except controls, SP, and specials.
165
  */
166
0
  if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
167
0
    return -1;
168
169
0
  for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
170
0
    if (IS_ATEXT(*ctx->data))
171
0
      continue;
172
173
0
    str_append_data(str, start, ctx->data - start);
174
0
    return rfc822_skip_lwsp(ctx);
175
0
  }
176
177
0
  str_append_data(str, start, ctx->data - start);
178
0
  return 0;
179
0
}
180
181
int rfc822_parse_dot_atom(struct rfc822_parser_context *ctx, string_t *str)
182
0
{
183
0
  const unsigned char *start;
184
0
  int ret;
185
186
  /*
187
     dot-atom        = [CFWS] dot-atom-text [CFWS]
188
     dot-atom-text   = 1*atext *("." 1*atext)
189
190
     atext           =
191
       ; Any character except controls, SP, and specials.
192
193
     For RFC-822 compatibility allow LWSP around '.'
194
  */
195
0
  if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
196
0
    return -1;
197
198
0
  for (start = ctx->data++; ctx->data < ctx->end; ) {
199
0
    if (IS_ATEXT(*ctx->data)) {
200
0
      ctx->data++;
201
0
      continue;
202
0
    }
203
204
0
    if (start == ctx->data)
205
0
      return -1;
206
0
    str_append_data(str, start, ctx->data - start);
207
208
0
    if ((ret = rfc822_skip_lwsp(ctx)) <= 0)
209
0
      return ret;
210
211
0
    if (*ctx->data != '.')
212
0
      return 1;
213
214
0
    ctx->data++;
215
0
    str_append_c(str, '.');
216
217
0
    if (rfc822_skip_lwsp(ctx) <= 0)
218
0
      return -1;
219
0
    start = ctx->data;
220
0
  }
221
222
0
  i_assert(start != ctx->data);
223
0
  str_append_data(str, start, ctx->data - start);
224
0
  return 0;
225
0
}
226
227
int rfc822_parse_mime_token(struct rfc822_parser_context *ctx, string_t *str)
228
444k
{
229
444k
  const unsigned char *start;
230
231
19.5M
  for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
232
19.5M
    if (IS_ATEXT_NON_TSPECIAL(*ctx->data) || *ctx->data == '.')
233
19.1M
      continue;
234
235
437k
    str_append_data(str, start, ctx->data - start);
236
437k
    return rfc822_skip_lwsp(ctx);
237
19.5M
  }
238
239
7.43k
  str_append_data(str, start, ctx->data - start);
240
7.43k
  return 0;
241
444k
}
242
243
int rfc822_parse_quoted_string(struct rfc822_parser_context *ctx, string_t *str)
244
8.35k
{
245
8.35k
  const unsigned char *start;
246
8.35k
  size_t len;
247
248
8.35k
  i_assert(ctx->data < ctx->end);
249
8.35k
  i_assert(*ctx->data == '"');
250
8.35k
  ctx->data++;
251
252
87.1M
  for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
253
87.1M
    switch (*ctx->data) {
254
72.6M
    case '\0':
255
72.6M
      if (ctx->nul_replacement_str != NULL) {
256
72.6M
        str_append_data(str, start, ctx->data - start);
257
72.6M
        str_append(str, ctx->nul_replacement_str);
258
72.6M
        start = ctx->data + 1;
259
72.6M
      }
260
72.6M
      break;
261
5.55k
    case '"':
262
5.55k
      str_append_data(str, start, ctx->data - start);
263
5.55k
      ctx->data++;
264
5.55k
      return rfc822_skip_lwsp(ctx);
265
3.02k
    case '\n':
266
      /* folding whitespace, remove the (CR)LF */
267
3.02k
      len = ctx->data - start;
268
3.02k
      if (len > 0 && start[len-1] == '\r')
269
711
        len--;
270
3.02k
      str_append_data(str, start, len);
271
3.02k
      start = ctx->data + 1;
272
3.02k
      break;
273
6.21k
    case '\\':
274
6.21k
      ctx->data++;
275
6.21k
      if (ctx->data >= ctx->end)
276
865
        return -1;
277
278
5.34k
      if (*ctx->data == '\r' || *ctx->data == '\n' ||
279
4.82k
          *ctx->data == '\0') {
280
        /* quoted-pair doesn't allow CR/LF/NUL.
281
           They are part of the obs-qp though, so don't
282
           return them as error. */
283
2.74k
        ctx->data--;
284
2.74k
        break;
285
2.74k
      }
286
2.60k
      str_append_data(str, start, ctx->data - start - 1);
287
2.60k
      start = ctx->data;
288
2.60k
      break;
289
87.1M
    }
290
87.1M
  }
291
292
  /* missing '"' */
293
1.93k
  return -1;
294
8.35k
}
295
296
static int
297
rfc822_parse_atom_or_dot(struct rfc822_parser_context *ctx, string_t *str)
298
0
{
299
0
  const unsigned char *start;
300
301
  /*
302
     atom            = [CFWS] 1*atext [CFWS]
303
     atext           =
304
       ; Any character except controls, SP, and specials.
305
306
     The difference between this function and rfc822_parse_dot_atom()
307
     is that this doesn't just silently skip over all the whitespace.
308
  */
309
0
  for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
310
0
    if (IS_ATEXT(*ctx->data) || *ctx->data == '.')
311
0
      continue;
312
313
0
    str_append_data(str, start, ctx->data - start);
314
0
    return rfc822_skip_lwsp(ctx);
315
0
  }
316
317
0
  str_append_data(str, start, ctx->data - start);
318
0
  return 0;
319
0
}
320
321
int rfc822_parse_phrase(struct rfc822_parser_context *ctx, string_t *str)
322
0
{
323
0
  int ret;
324
325
  /*
326
     phrase     = 1*word / obs-phrase
327
     word       = atom / quoted-string
328
     obs-phrase = word *(word / "." / CFWS)
329
  */
330
331
0
  if (ctx->data >= ctx->end)
332
0
    return 0;
333
0
  if (*ctx->data == '.')
334
0
    return -1;
335
336
0
  for (;;) {
337
0
    if (*ctx->data == '"')
338
0
      ret = rfc822_parse_quoted_string(ctx, str);
339
0
    else
340
0
      ret = rfc822_parse_atom_or_dot(ctx, str);
341
342
0
    if (ret <= 0)
343
0
      return ret;
344
345
0
    if (!IS_ATEXT(*ctx->data) && *ctx->data != '"'
346
0
        && *ctx->data != '.')
347
0
      break;
348
0
    str_append_c(str, ' ');
349
0
  }
350
0
  return rfc822_skip_lwsp(ctx);
351
0
}
352
353
static int
354
rfc822_parse_domain_literal(struct rfc822_parser_context *ctx, string_t *str)
355
0
{
356
0
  const unsigned char *start;
357
0
  size_t len;
358
359
  /*
360
     domain-literal  = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
361
     dcontent        = dtext / quoted-pair
362
     dtext           = NO-WS-CTL /     ; Non white space controls
363
           %d33-90 /       ; The rest of the US-ASCII
364
           %d94-126        ;  characters not including "[",
365
               ;  "]", or "\"
366
  */
367
0
  i_assert(ctx->data < ctx->end);
368
0
  i_assert(*ctx->data == '[');
369
370
0
  for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
371
0
    switch (*ctx->data) {
372
0
    case '\0':
373
0
      if (ctx->nul_replacement_str != NULL) {
374
0
        str_append_data(str, start, ctx->data - start);
375
0
        str_append(str, ctx->nul_replacement_str);
376
0
        start = ctx->data + 1;
377
0
      }
378
0
      break;
379
0
    case '[':
380
      /* not allowed */
381
0
      return -1;
382
0
    case ']':
383
0
      str_append_data(str, start, ctx->data - start + 1);
384
0
      ctx->data++;
385
0
      return rfc822_skip_lwsp(ctx);
386
0
    case '\n':
387
      /* folding whitespace, remove the (CR)LF */
388
0
      len = ctx->data - start;
389
0
      if (len > 0 && start[len-1] == '\r')
390
0
        len--;
391
0
      str_append_data(str, start, len);
392
0
      start = ctx->data + 1;
393
0
      break;
394
0
    case '\\':
395
      /* note: the '\' is preserved in the output */
396
0
      ctx->data++;
397
0
      if (ctx->data >= ctx->end)
398
0
        return -1;
399
400
0
      if (*ctx->data == '\r' || *ctx->data == '\n' ||
401
0
          *ctx->data == '\0') {
402
        /* quoted-pair doesn't allow CR/LF/NUL.
403
           They are part of the obs-qp though, so don't
404
           return them as error. */
405
0
        str_append_data(str, start, ctx->data - start);
406
0
        start = ctx->data;
407
0
        ctx->data--;
408
0
        break;
409
0
      }
410
0
    }
411
0
  }
412
413
  /* missing ']' */
414
0
  return -1;
415
0
}
416
417
int rfc822_parse_domain(struct rfc822_parser_context *ctx, string_t *str)
418
0
{
419
  /*
420
     domain          = dot-atom / domain-literal / obs-domain
421
     domain-literal  = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
422
     obs-domain      = atom *("." atom)
423
  */
424
0
  i_assert(ctx->data < ctx->end);
425
0
  i_assert(*ctx->data == '@');
426
0
  ctx->data++;
427
428
0
  if (rfc822_skip_lwsp(ctx) <= 0)
429
0
    return -1;
430
431
0
  if (*ctx->data == '[')
432
0
    return rfc822_parse_domain_literal(ctx, str);
433
0
  else {
434
#ifdef EXPERIMENTAL_MAIL_UTF8
435
    size_t start_pos = str_len(str);
436
#endif
437
0
    int ret = rfc822_parse_dot_atom(ctx, str);
438
#ifdef EXPERIMENTAL_MAIL_UTF8
439
    if (ret < 0)
440
      return -1;
441
442
    const char *domain = str_c(str) + start_pos, *domain_unicode;
443
    const char *error;
444
445
    if (idna_process_domain_name(domain, 0, &domain_unicode, NULL,
446
               &error) == 0) {
447
      str_truncate(str, start_pos);
448
      str_append(str, domain_unicode);
449
    }
450
#endif
451
0
    return ret;
452
0
  }
453
0
}
454
455
int rfc822_parse_content_type(struct rfc822_parser_context *ctx, string_t *str)
456
48.9k
{
457
48.9k
  size_t str_pos_0 = str->used;
458
48.9k
  if (rfc822_skip_lwsp(ctx) <= 0)
459
3.03k
    return -1;
460
461
  /* get main type, require at least one byte */
462
45.9k
  if (rfc822_parse_mime_token(ctx, str) <= 0 ||
463
43.7k
      str->used == str_pos_0)
464
2.65k
    return -1;
465
466
  /* skip over "/" */
467
43.2k
  if (*ctx->data != '/') {
468
2.09k
    str_truncate(str, str_pos_0);
469
2.09k
    return -1;
470
2.09k
  }
471
41.1k
  ctx->data++;
472
41.1k
  if (rfc822_skip_lwsp(ctx) <= 0) {
473
4.81k
    str_truncate(str, str_pos_0);
474
4.81k
    return -1;
475
4.81k
  }
476
36.3k
  str_append_c(str, '/');
477
478
36.3k
  size_t str_pos = str->used;
479
  /* get subtype, require at least one byte,
480
     and check the next separator to avoid accepting
481
     invalid values. */
482
36.3k
  int ret;
483
36.3k
  if ((ret = rfc822_parse_mime_token(ctx, str)) < 0 ||
484
35.8k
      str->used == str_pos ||
485
35.2k
      (ctx->data != ctx->end && *ctx->data != ';')) {
486
1.65k
    str_truncate(str, str_pos_0);
487
1.65k
    return -1;
488
1.65k
  }
489
34.6k
  return ret;
490
36.3k
}
491
492
int rfc822_parse_content_param(struct rfc822_parser_context *ctx,
493
             const char **key_r, string_t *value)
494
514k
{
495
514k
  string_t *key;
496
514k
  int ret;
497
498
  /* .. := *(";" parameter)
499
     parameter := attribute "=" value
500
     attribute := token
501
     value := token / quoted-string
502
  */
503
514k
  *key_r = NULL;
504
514k
  str_truncate(value, 0);
505
506
514k
  if (ctx->data >= ctx->end)
507
22.7k
    return 0;
508
491k
  if (*ctx->data != ';')
509
283k
    return -1;
510
207k
  ctx->data++;
511
512
207k
  if (rfc822_skip_lwsp(ctx) <= 0)
513
3.19k
    return -1;
514
515
204k
  key = t_str_new(64);
516
204k
  if (rfc822_parse_mime_token(ctx, key) <= 0)
517
1.83k
    return -1;
518
519
202k
  if (*ctx->data != '=')
520
18.4k
    return -1;
521
184k
  ctx->data++;
522
523
184k
  if ((ret = rfc822_skip_lwsp(ctx)) <= 0) {
524
    /* broken / no value */
525
169k
  } else if (*ctx->data == '"') {
526
8.35k
    ret = rfc822_parse_quoted_string(ctx, value);
527
161k
  } else if (ctx->data < ctx->end && *ctx->data == '=') {
528
    /* workaround for broken input:
529
       name==?utf-8?b?...?= */
530
23.7k
    while (ctx->data < ctx->end && *ctx->data != ';' &&
531
21.0k
           *ctx->data != ' ' && *ctx->data != '\t' &&
532
20.3k
           *ctx->data != '\r' && *ctx->data != '\n') {
533
20.1k
      str_append_c(value, *ctx->data);
534
20.1k
      ctx->data++;
535
20.1k
    }
536
157k
  } else {
537
157k
    ret = rfc822_parse_mime_token(ctx, value);
538
157k
  }
539
540
184k
  *key_r = str_c(key);
541
184k
  return ret < 0 ? -1 : 1;
542
202k
}