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