/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(×tamp); |
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 | } |