Coverage Report

Created: 2025-07-18 06:24

/src/dovecot/src/lib-mail/message-date.c
Line
Count
Source (jump to first uncovered line)
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
0
  ((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
0
{
33
0
  int offset;
34
0
  char chr;
35
36
0
  if (len == 5 && (*str == '+' || *str == '-')) {
37
    /* numeric offset */
38
0
    if (!i_isdigit(str[1]) || !i_isdigit(str[2]) ||
39
0
        !i_isdigit(str[3]) || !i_isdigit(str[4]))
40
0
      return 0;
41
42
0
    offset = ((str[1]-'0') * 10 + (str[2]-'0')) * 60  +
43
0
      (str[3]-'0') * 10 + (str[4]-'0');
44
0
    return *str == '+' ? offset : -offset;
45
0
  }
46
47
0
  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
0
    chr = i_toupper(*str);
52
0
    if (chr < 'J')
53
0
      return (*str-'A'+1) * 60;
54
0
    if (chr == 'J')
55
0
      return 0;
56
0
    if (chr <= 'M')
57
0
      return (*str-'A') * 60;
58
0
    if (chr < 'Z')
59
0
      return ('M'-*str) * 60;
60
0
    return 0;
61
0
  }
62
63
0
  if (len == 2 && i_toupper(str[0]) == 'U' && i_toupper(str[1]) == 'T') {
64
    /* UT - Universal Time */
65
0
    return 0;
66
0
  }
67
68
0
  if (len == 3) {
69
    /* GMT | [ECMP][DS]T */
70
0
    if (str[2] != 'T')
71
0
      return 0;
72
73
0
    switch (i_toupper(*str)) {
74
0
    case 'E':
75
0
      offset = -5 * 60;
76
0
      break;
77
0
    case 'C':
78
0
      offset = -6 * 60;
79
0
      break;
80
0
    case 'M':
81
0
      offset = -7 * 60;
82
0
      break;
83
0
    case 'P':
84
0
      offset = -8 * 60;
85
0
      break;
86
0
    default:
87
      /* GMT and others */
88
0
      return 0;
89
0
    }
90
91
0
    if (i_toupper(str[1]) == 'D')
92
0
      return offset + 60;
93
0
    if (i_toupper(str[1]) == 'S')
94
0
      return offset;
95
0
  }
96
97
0
  return 0;
98
0
}
99
100
static int next_token(struct message_date_parser_context *ctx,
101
          const unsigned char **value, size_t *value_len)
102
0
{
103
0
  int ret;
104
105
0
  str_truncate(ctx->str, 0);
106
0
  ret = ctx->parser.data >= ctx->parser.end ? 0 :
107
0
    rfc822_parse_atom(&ctx->parser, ctx->str);
108
109
0
  *value = str_data(ctx->str);
110
0
  *value_len = str_len(ctx->str);
111
0
  return ret < 0 ? -1 : *value_len > 0;
112
0
}
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
0
{
118
0
  struct tm tm;
119
0
  const unsigned char *value;
120
0
  size_t i, len;
121
0
  int ret;
122
123
  /* [weekday_name "," ] dd month_name [yy]yy hh:mi[:ss] timezone */
124
0
  i_zero(&tm);
125
126
0
        rfc822_skip_lwsp(&ctx->parser);
127
128
  /* skip the optional weekday */
129
0
  if (next_token(ctx, &value, &len) <= 0)
130
0
    return FALSE;
131
0
  if (len == 3) {
132
0
    if (*ctx->parser.data != ',')
133
0
      return FALSE;
134
0
    ctx->parser.data++;
135
0
    rfc822_skip_lwsp(&ctx->parser);
136
137
0
    if (next_token(ctx, &value, &len) <= 0)
138
0
      return FALSE;
139
0
  }
140
141
  /* dd */
142
0
  if (len < 1 || len > 2 || !i_isdigit(value[0]))
143
0
    return FALSE;
144
145
0
  tm.tm_mday = value[0]-'0';
146
0
  if (len == 2) {
147
0
    if (!i_isdigit(value[1]))
148
0
      return FALSE;
149
0
    tm.tm_mday = (tm.tm_mday * 10) + (value[1]-'0');
150
0
  }
151
152
  /* month name */
153
0
  if (next_token(ctx, &value, &len) <= 0 || len < 3)
154
0
    return FALSE;
155
156
0
  for (i = 0; i < 12; i++) {
157
0
    if (i_memcasecmp(month_names[i], value, 3) == 0) {
158
0
      tm.tm_mon = i;
159
0
      break;
160
0
    }
161
0
  }
162
0
  if (i == 12)
163
0
    return FALSE;
164
165
  /* [yy]yy */
166
0
  if (next_token(ctx, &value, &len) <= 0 || (len != 2 && len != 4))
167
0
    return FALSE;
168
169
0
  for (i = 0; i < len; i++) {
170
0
    if (!i_isdigit(value[i]))
171
0
      return FALSE;
172
0
    tm.tm_year = tm.tm_year * 10 + (value[i]-'0');
173
0
  }
174
175
0
  if (len == 2) {
176
    /* two digit year, assume 1970+ */
177
0
    if (tm.tm_year < 70)
178
0
      tm.tm_year += 100;
179
0
  } else {
180
0
    if (tm.tm_year < 1900)
181
0
      return FALSE;
182
0
    tm.tm_year -= 1900;
183
0
  }
184
185
  /* hh, allow also single digit */
186
0
  if (next_token(ctx, &value, &len) <= 0 ||
187
0
      len < 1 || len > 2 || !i_isdigit(value[0]))
188
0
    return FALSE;
189
0
  tm.tm_hour = value[0]-'0';
190
0
  if (len == 2) {
191
0
    if (!i_isdigit(value[1]))
192
0
      return FALSE;
193
0
    tm.tm_hour = tm.tm_hour * 10 + (value[1]-'0');
194
0
  }
195
196
  /* :mm (may be the last token) */
197
0
  if (!IS_TIME_SEP(*ctx->parser.data))
198
0
    return FALSE;
199
0
  ctx->parser.data++;
200
0
  rfc822_skip_lwsp(&ctx->parser);
201
202
0
  if (next_token(ctx, &value, &len) < 0 || len != 2 ||
203
0
      !i_isdigit(value[0]) || !i_isdigit(value[1]))
204
0
    return FALSE;
205
0
  tm.tm_min = (value[0]-'0') * 10 + (value[1]-'0');
206
207
  /* [:ss] */
208
0
  if (ctx->parser.data < ctx->parser.end &&
209
0
      IS_TIME_SEP(*ctx->parser.data)) {
210
0
    ctx->parser.data++;
211
0
    rfc822_skip_lwsp(&ctx->parser);
212
213
0
    if (next_token(ctx, &value, &len) <= 0 || len != 2 ||
214
0
        !i_isdigit(value[0]) || !i_isdigit(value[1]))
215
0
      return FALSE;
216
0
    tm.tm_sec = (value[0]-'0') * 10 + (value[1]-'0');
217
0
  }
218
219
0
  if ((ret = next_token(ctx, &value, &len)) < 0)
220
0
    return FALSE;
221
0
  if (ret == 0) {
222
    /* missing timezone */
223
0
    *timezone_offset_r = 0;
224
0
  } else {
225
    /* timezone. invalid timezones are treated as GMT, because
226
       we may not know all the possible timezones that are used
227
       and it's better to give at least a mostly correct reply.
228
       FIXME: perhaps some different strict version of this
229
       function would be useful? */
230
0
    *timezone_offset_r = parse_timezone(value, len);
231
0
  }
232
233
0
  tm.tm_isdst = -1;
234
0
  *timestamp_r = utc_mktime(&tm);
235
0
  if (*timestamp_r == (time_t)-1)
236
0
    return FALSE;
237
238
0
  *timestamp_r -= *timezone_offset_r * 60;
239
240
0
  return TRUE;
241
0
}
242
243
bool message_date_parse(const unsigned char *data, size_t size,
244
      time_t *timestamp_r, int *timezone_offset_r)
245
0
{
246
0
  bool success;
247
248
0
  T_BEGIN {
249
0
    struct message_date_parser_context ctx;
250
251
0
    rfc822_parser_init(&ctx.parser, data, size, NULL);
252
0
    ctx.str = t_str_new(128);
253
0
    success = message_date_parser_tokens(&ctx, timestamp_r,
254
0
                 timezone_offset_r);
255
0
    rfc822_parser_deinit(&ctx.parser);
256
0
  } T_END;
257
258
0
  return success;
259
0
}
260
261
const char *message_date_create(time_t timestamp)
262
0
{
263
0
  struct tm *tm;
264
0
  int offset;
265
0
  bool negative;
266
267
0
  tm = localtime(&timestamp);
268
0
  offset = utc_offset(tm, timestamp);
269
0
  if (offset >= 0)
270
0
    negative = FALSE;
271
0
  else {
272
0
    negative = TRUE;
273
0
    offset = -offset;
274
0
  }
275
276
0
  return t_strdup_printf("%s, %02d %s %04d %02d:%02d:%02d %c%02d%02d",
277
0
             weekday_names[tm->tm_wday],
278
0
             tm->tm_mday,
279
0
             month_names[tm->tm_mon],
280
0
             tm->tm_year+1900,
281
0
             tm->tm_hour, tm->tm_min, tm->tm_sec,
282
0
             negative ? '-' : '+', offset / 60, offset % 60);
283
0
}