Coverage Report

Created: 2026-05-30 06:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-mail/message-date.c
Line
Count
Source
1
/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
2
3
#include "lib.h"
4
#include "str.h"
5
#include "utc-offset.h"
6
#include "utc-mktime.h"
7
#include "rfc822-parser.h"
8
#include "message-date.h"
9
10
#include <ctype.h>
11
12
/* RFC specifies ':' as the only allowed separator,
13
   but be forgiving also for some broken ones */
14
#define IS_TIME_SEP(c) \
15
573
  ((c) == ':' || (c) == '.')
16
17
struct message_date_parser_context {
18
  struct rfc822_parser_context parser;
19
  string_t *str;
20
};
21
22
static const char *month_names[] = {
23
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
24
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
25
};
26
27
static const char *weekday_names[] = {
28
  "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
29
};
30
31
static int parse_timezone(const unsigned char *str, size_t len)
32
164
{
33
164
  int offset;
34
164
  char chr;
35
36
164
  if (len == 5 && (*str == '+' || *str == '-')) {
37
    /* numeric offset */
38
6
    if (!i_isdigit(str[1]) || !i_isdigit(str[2]) ||
39
4
        !i_isdigit(str[3]) || !i_isdigit(str[4]))
40
4
      return 0;
41
42
2
    offset = ((str[1]-'0') * 10 + (str[2]-'0')) * 60  +
43
2
      (str[3]-'0') * 10 + (str[4]-'0');
44
2
    return *str == '+' ? offset : -offset;
45
6
  }
46
47
158
  if (len == 1) {
48
    /* military zone - handle them the correct way, not as
49
       RFC822 says. RFC2822 though suggests that they'd be
50
       considered as unspecified.. */
51
32
    chr = i_toupper(*str);
52
32
    if (chr < 'J')
53
19
      return (*str-'A'+1) * 60;
54
13
    if (chr == 'J')
55
1
      return 0;
56
12
    if (chr <= 'M')
57
2
      return (*str-'A') * 60;
58
10
    if (chr < 'Z')
59
8
      return ('M'-*str) * 60;
60
2
    return 0;
61
10
  }
62
63
126
  if (len == 2 && i_toupper(str[0]) == 'U' && i_toupper(str[1]) == 'T') {
64
    /* UT - Universal Time */
65
1
    return 0;
66
1
  }
67
68
125
  if (len == 3) {
69
    /* GMT | [ECMP][DS]T */
70
29
    if (str[2] != 'T')
71
11
      return 0;
72
73
18
    switch (i_toupper(*str)) {
74
6
    case 'E':
75
6
      offset = -5 * 60;
76
6
      break;
77
4
    case 'C':
78
4
      offset = -6 * 60;
79
4
      break;
80
2
    case 'M':
81
2
      offset = -7 * 60;
82
2
      break;
83
5
    case 'P':
84
5
      offset = -8 * 60;
85
5
      break;
86
1
    default:
87
      /* GMT and others */
88
1
      return 0;
89
18
    }
90
91
17
    if (i_toupper(str[1]) == 'D')
92
1
      return offset + 60;
93
16
    if (i_toupper(str[1]) == 'S')
94
1
      return offset;
95
16
  }
96
97
111
  return 0;
98
125
}
99
100
static int next_token(struct message_date_parser_context *ctx,
101
          const unsigned char **value, size_t *value_len)
102
3.61k
{
103
3.61k
  int ret;
104
105
3.61k
  str_truncate(ctx->str, 0);
106
3.61k
  ret = ctx->parser.data >= ctx->parser.end ? 0 :
107
3.61k
    rfc822_parse_atom(&ctx->parser, ctx->str);
108
109
3.61k
  *value = str_data(ctx->str);
110
3.61k
  *value_len = str_len(ctx->str);
111
3.61k
  return ret < 0 ? -1 : *value_len > 0;
112
3.61k
}
113
114
static bool
115
message_date_parser_tokens(struct message_date_parser_context *ctx,
116
         time_t *timestamp_r, int *timezone_offset_r)
117
1.14k
{
118
1.14k
  struct tm tm;
119
1.14k
  const unsigned char *value;
120
1.14k
  size_t i, len;
121
1.14k
  int ret;
122
123
  /* [weekday_name "," ] dd month_name [yy]yy hh:mi[:ss] timezone */
124
1.14k
  i_zero(&tm);
125
126
1.14k
        rfc822_skip_lwsp(&ctx->parser);
127
128
  /* skip the optional weekday */
129
1.14k
  if (next_token(ctx, &value, &len) <= 0)
130
185
    return FALSE;
131
963
  if (len == 3) {
132
51
    if (ctx->parser.data == ctx->parser.end ||
133
50
        *ctx->parser.data != ',')
134
13
      return FALSE;
135
38
    ctx->parser.data++;
136
38
    rfc822_skip_lwsp(&ctx->parser);
137
138
38
    if (next_token(ctx, &value, &len) <= 0)
139
2
      return FALSE;
140
38
  }
141
142
  /* dd */
143
948
  if (len < 1 || len > 2 || !i_isdigit(value[0]))
144
249
    return FALSE;
145
146
699
  tm.tm_mday = value[0]-'0';
147
699
  if (len == 2) {
148
23
    if (!i_isdigit(value[1]))
149
1
      return FALSE;
150
22
    tm.tm_mday = (tm.tm_mday * 10) + (value[1]-'0');
151
22
  }
152
153
  /* month name */
154
698
  if (next_token(ctx, &value, &len) <= 0 || len < 3)
155
34
    return FALSE;
156
157
4.06k
  for (i = 0; i < 12; i++) {
158
4.05k
    if (i_memcasecmp(month_names[i], value, 3) == 0) {
159
649
      tm.tm_mon = i;
160
649
      break;
161
649
    }
162
4.05k
  }
163
664
  if (i == 12)
164
15
    return FALSE;
165
166
  /* [yy]yy */
167
649
  if (next_token(ctx, &value, &len) <= 0 || (len != 2 && len != 4))
168
219
    return FALSE;
169
170
1.43k
  for (i = 0; i < len; i++) {
171
1.01k
    if (!i_isdigit(value[i]))
172
2
      return FALSE;
173
1.00k
    tm.tm_year = tm.tm_year * 10 + (value[i]-'0');
174
1.00k
  }
175
176
428
  if (len == 2) {
177
    /* two digit year, assume 1970+ */
178
353
    if (tm.tm_year < 70)
179
265
      tm.tm_year += 100;
180
353
  } else {
181
75
    if (tm.tm_year < 1900)
182
12
      return FALSE;
183
63
    tm.tm_year -= 1900;
184
63
  }
185
186
  /* hh, allow also single digit */
187
416
  if (next_token(ctx, &value, &len) <= 0 ||
188
393
      len < 1 || len > 2 || !i_isdigit(value[0]))
189
57
    return FALSE;
190
359
  tm.tm_hour = value[0]-'0';
191
359
  if (len == 2) {
192
16
    if (!i_isdigit(value[1]))
193
1
      return FALSE;
194
15
    tm.tm_hour = tm.tm_hour * 10 + (value[1]-'0');
195
15
  }
196
197
  /* :mm (may be the last token) */
198
358
  if (ctx->parser.data == ctx->parser.end ||
199
356
      !IS_TIME_SEP(*ctx->parser.data))
200
18
    return FALSE;
201
340
  ctx->parser.data++;
202
340
  rfc822_skip_lwsp(&ctx->parser);
203
204
340
  if (next_token(ctx, &value, &len) < 0 || len != 2 ||
205
315
      !i_isdigit(value[0]) || !i_isdigit(value[1]))
206
27
    return FALSE;
207
313
  tm.tm_min = (value[0]-'0') * 10 + (value[1]-'0');
208
209
  /* [:ss] */
210
313
  if (ctx->parser.data < ctx->parser.end &&
211
217
      IS_TIME_SEP(*ctx->parser.data)) {
212
44
    ctx->parser.data++;
213
44
    rfc822_skip_lwsp(&ctx->parser);
214
215
44
    if (next_token(ctx, &value, &len) <= 0 || len != 2 ||
216
19
        !i_isdigit(value[0]) || !i_isdigit(value[1]))
217
27
      return FALSE;
218
17
    tm.tm_sec = (value[0]-'0') * 10 + (value[1]-'0');
219
17
  }
220
221
286
  if ((ret = next_token(ctx, &value, &len)) < 0)
222
12
    return FALSE;
223
274
  if (ret == 0) {
224
    /* missing timezone */
225
110
    *timezone_offset_r = 0;
226
164
  } else {
227
    /* timezone. invalid timezones are treated as GMT, because
228
       we may not know all the possible timezones that are used
229
       and it's better to give at least a mostly correct reply.
230
       FIXME: perhaps some different strict version of this
231
       function would be useful? */
232
164
    *timezone_offset_r = parse_timezone(value, len);
233
164
  }
234
235
274
  tm.tm_isdst = -1;
236
274
  *timestamp_r = utc_mktime(&tm);
237
274
  if (*timestamp_r == (time_t)-1)
238
94
    return FALSE;
239
240
180
  *timestamp_r -= *timezone_offset_r * 60;
241
242
180
  return TRUE;
243
274
}
244
245
bool message_date_parse(const unsigned char *data, size_t size,
246
      time_t *timestamp_r, int *timezone_offset_r)
247
1.14k
{
248
1.14k
  bool success;
249
250
1.14k
  T_BEGIN {
251
1.14k
    struct message_date_parser_context ctx;
252
253
1.14k
    rfc822_parser_init(&ctx.parser, data, size, NULL);
254
1.14k
    ctx.str = t_str_new(128);
255
1.14k
    success = message_date_parser_tokens(&ctx, timestamp_r,
256
1.14k
                 timezone_offset_r);
257
1.14k
    rfc822_parser_deinit(&ctx.parser);
258
1.14k
  } T_END;
259
260
1.14k
  return success;
261
1.14k
}
262
263
const char *message_date_create(time_t timestamp)
264
0
{
265
0
  struct tm *tm;
266
0
  int offset;
267
0
  bool negative;
268
269
0
  tm = localtime(&timestamp);
270
0
  offset = utc_offset(tm, timestamp);
271
0
  if (offset >= 0)
272
0
    negative = FALSE;
273
0
  else {
274
0
    negative = TRUE;
275
0
    offset = -offset;
276
0
  }
277
278
0
  return t_strdup_printf("%s, %02d %s %04d %02d:%02d:%02d %c%02d%02d",
279
0
             weekday_names[tm->tm_wday],
280
0
             tm->tm_mday,
281
0
             month_names[tm->tm_mon],
282
0
             tm->tm_year+1900,
283
0
             tm->tm_hour, tm->tm_min, tm->tm_sec,
284
0
             negative ? '-' : '+', offset / 60, offset % 60);
285
0
}