/src/pigeonhole/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c
Line | Count | Source |
1 | | /* Copyright (c) Pigeonhole authors, see top-level COPYING file */ |
2 | | |
3 | | #include "lib.h" |
4 | | #include "strfuncs.h" |
5 | | #include "mail-storage.h" |
6 | | #include "settings.h" |
7 | | |
8 | | #include "sieve-common.h" |
9 | | #include "sieve-error.h" |
10 | | #include "sieve-extensions.h" |
11 | | #include "sieve-message.h" |
12 | | #include "sieve-interpreter.h" |
13 | | #include "sieve-runtime-trace.h" |
14 | | |
15 | | #include "ext-spamvirustest-settings.h" |
16 | | #include "ext-spamvirustest-common.h" |
17 | | |
18 | | #include "dregex.h" |
19 | | |
20 | | #include <sys/types.h> |
21 | | #include <ctype.h> |
22 | | |
23 | | /* |
24 | | * Extension data |
25 | | */ |
26 | | |
27 | | struct ext_spamvirustest_header_spec { |
28 | | const char *header_name; |
29 | | struct dregex_code *regexp; |
30 | | bool regexp_match; |
31 | | }; |
32 | | |
33 | | struct ext_spamvirustest_context { |
34 | | pool_t pool; |
35 | | unsigned int reload_id; |
36 | | const struct ext_spamvirustest_settings *set; |
37 | | |
38 | | struct ext_spamvirustest_header_spec status_header; |
39 | | struct ext_spamvirustest_header_spec score_max_header; |
40 | | }; |
41 | | |
42 | | /* |
43 | | * Configuration parser |
44 | | */ |
45 | | |
46 | | static bool |
47 | | ext_spamvirustest_header_spec_parse( |
48 | | pool_t pool, const char *data, |
49 | | struct ext_spamvirustest_header_spec *spec_r, const char **error_r) |
50 | 0 | { |
51 | 0 | const char *p; |
52 | 0 | const char *regexp_error; |
53 | |
|
54 | 0 | if (*data == '\0') { |
55 | 0 | *error_r = "empty header specification"; |
56 | 0 | return FALSE; |
57 | 0 | } |
58 | | |
59 | | /* Parse header name */ |
60 | | |
61 | 0 | p = data; |
62 | |
|
63 | 0 | while (*p == ' ' || *p == '\t') |
64 | 0 | p++; |
65 | 0 | while (*p != ':' && *p != '\0' && *p != ' ' && *p != '\t') |
66 | 0 | p++; |
67 | |
|
68 | 0 | if (*p == '\0') { |
69 | 0 | spec_r->header_name = p_strdup(pool, data); |
70 | 0 | return TRUE; |
71 | 0 | } |
72 | | |
73 | 0 | spec_r->header_name = p_strdup_until(pool, data, p); |
74 | 0 | while (*p == ' ' || *p == '\t') |
75 | 0 | p++; |
76 | |
|
77 | 0 | if (*p == '\0') { |
78 | 0 | spec_r->regexp_match = FALSE; |
79 | 0 | return TRUE; |
80 | 0 | } |
81 | | |
82 | | /* Parse and compile regular expression */ |
83 | | |
84 | 0 | if (*p != ':') { |
85 | 0 | *error_r = t_strdup_printf("expecting ':', but found '%c'", *p); |
86 | 0 | return FALSE; |
87 | 0 | } |
88 | 0 | p++; |
89 | 0 | while (*p == ' ' || *p == '\t') p++; |
90 | |
|
91 | 0 | spec_r->regexp_match = TRUE; |
92 | 0 | struct dregex_code *code = dregex_code_create(); |
93 | |
|
94 | 0 | if (dregex_code_compile(code, p, DREGEX_ICASE|DREGEX_ASCII_ONLY, ®exp_error) < 0) { |
95 | 0 | *error_r = t_strdup_printf( |
96 | 0 | "failed to compile regular expression '%s': %s", |
97 | 0 | p, regexp_error); |
98 | 0 | dregex_code_free(&code); |
99 | 0 | return FALSE; |
100 | 0 | } |
101 | 0 | spec_r->regexp = code; |
102 | 0 | return TRUE; |
103 | 0 | } |
104 | | |
105 | | static void |
106 | | ext_spamvirustest_header_spec_free(struct ext_spamvirustest_header_spec *spec) |
107 | 0 | { |
108 | 0 | dregex_code_free(&spec->regexp); |
109 | 0 | } |
110 | | |
111 | | static bool |
112 | | ext_spamvirustest_parse_strlen_value(const char *str_value, |
113 | | float *value_r, const char **error_r) |
114 | 0 | { |
115 | 0 | const char *p = str_value; |
116 | 0 | char ch = *p; |
117 | |
|
118 | 0 | if (*str_value == '\0') { |
119 | 0 | *value_r = 0; |
120 | 0 | return TRUE; |
121 | 0 | } |
122 | | |
123 | 0 | while (*p == ch) |
124 | 0 | p++; |
125 | |
|
126 | 0 | if (*p != '\0') { |
127 | 0 | *error_r = t_strdup_printf( |
128 | 0 | "different character '%c' encountered in strlen value", |
129 | 0 | *p); |
130 | 0 | return FALSE; |
131 | 0 | } |
132 | | |
133 | 0 | *value_r = (p - str_value); |
134 | 0 | return TRUE; |
135 | 0 | } |
136 | | |
137 | | /* |
138 | | * Extension initialization |
139 | | */ |
140 | | |
141 | | int ext_spamvirustest_load(const struct sieve_extension *ext, void **context_r) |
142 | 0 | { |
143 | 0 | static unsigned int reload_id = 0; |
144 | 0 | struct sieve_instance *svinst = ext->svinst; |
145 | 0 | struct ext_spamvirustest_context *extctx; |
146 | 0 | const struct setting_parser_info *set_info; |
147 | 0 | const struct ext_spamvirustest_settings *set; |
148 | 0 | const char *error; |
149 | 0 | pool_t pool; |
150 | 0 | int ret = 0; |
151 | | |
152 | | /* Get settings */ |
153 | |
|
154 | 0 | if (sieve_extension_is(ext, spamtest_extension) || |
155 | 0 | sieve_extension_is(ext, spamtestplus_extension)) |
156 | 0 | set_info = &ext_spamtest_setting_parser_info; |
157 | 0 | else if (sieve_extension_is(ext, virustest_extension)) |
158 | 0 | set_info = &ext_virustest_setting_parser_info; |
159 | 0 | else |
160 | 0 | i_unreached(); |
161 | | |
162 | 0 | if (settings_get(svinst->event, set_info, 0, &set, &error) < 0) { |
163 | 0 | e_error(svinst->event, "%s", error); |
164 | 0 | return -1; |
165 | 0 | } |
166 | | |
167 | | /* Base configuration */ |
168 | | |
169 | 0 | if (*set->status_header == '\0') { |
170 | 0 | settings_free(set); |
171 | 0 | return 0; |
172 | 0 | } |
173 | | |
174 | 0 | pool = pool_alloconly_create("spamvirustest_data", 512); |
175 | 0 | extctx = p_new(pool, struct ext_spamvirustest_context, 1); |
176 | 0 | extctx->pool = pool; |
177 | 0 | extctx->reload_id = ++reload_id; |
178 | 0 | extctx->set = set; |
179 | |
|
180 | 0 | if (!ext_spamvirustest_header_spec_parse(extctx->pool, |
181 | 0 | set->status_header, |
182 | 0 | &extctx->status_header, |
183 | 0 | &error)) { |
184 | 0 | e_error(svinst->event, "%s: " |
185 | 0 | "Invalid status header specification '%s': %s", |
186 | 0 | sieve_extension_name(ext), set->status_header, error); |
187 | 0 | ret = -1; |
188 | 0 | } |
189 | |
|
190 | 0 | if (ret == 0 && |
191 | 0 | set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT && |
192 | 0 | *set->score_max_header != '\0' && |
193 | 0 | !ext_spamvirustest_header_spec_parse(extctx->pool, |
194 | 0 | set->score_max_header, |
195 | 0 | &extctx->score_max_header, |
196 | 0 | &error)) { |
197 | 0 | e_error(svinst->event, "%s: " |
198 | 0 | "Invalid max score header specification '%s': %s", |
199 | 0 | sieve_extension_name(ext), set->score_max_header, error); |
200 | 0 | ret = -1; |
201 | 0 | } |
202 | |
|
203 | 0 | *context_r = extctx; |
204 | 0 | if (ret < 0) { |
205 | 0 | e_warning(svinst->event, "%s: " |
206 | 0 | "Extension not configured, " |
207 | 0 | "tests will always match against \"0\"", |
208 | 0 | sieve_extension_name(ext)); |
209 | 0 | ext_spamvirustest_unload(ext); |
210 | 0 | *context_r = NULL; |
211 | 0 | } |
212 | |
|
213 | 0 | return ret; |
214 | 0 | } |
215 | | |
216 | | void ext_spamvirustest_unload(const struct sieve_extension *ext) |
217 | 0 | { |
218 | 0 | struct ext_spamvirustest_context *extctx = ext->context; |
219 | |
|
220 | 0 | if (extctx == NULL) |
221 | 0 | return; |
222 | | |
223 | 0 | ext_spamvirustest_header_spec_free(&extctx->status_header); |
224 | 0 | ext_spamvirustest_header_spec_free(&extctx->score_max_header); |
225 | 0 | settings_free(extctx->set); |
226 | 0 | pool_unref(&extctx->pool); |
227 | 0 | } |
228 | | |
229 | | /* |
230 | | * Runtime |
231 | | */ |
232 | | |
233 | | struct ext_spamvirustest_message_context { |
234 | | unsigned int reload_id; |
235 | | float score_ratio; |
236 | | }; |
237 | | |
238 | | static const char * |
239 | | ext_spamvirustest_get_score(const struct sieve_extension *ext, |
240 | | float score_ratio, bool percent) |
241 | 0 | { |
242 | 0 | int score; |
243 | |
|
244 | 0 | if (score_ratio < 0) |
245 | 0 | return "0"; |
246 | | |
247 | 0 | if (score_ratio > 1) |
248 | 0 | score_ratio = 1; |
249 | |
|
250 | 0 | if (percent) |
251 | 0 | score = score_ratio * 100 + 0.001; |
252 | 0 | else if (sieve_extension_is(ext, virustest_extension)) |
253 | 0 | score = score_ratio * 4 + 1.001; |
254 | 0 | else |
255 | 0 | score = score_ratio * 9 + 1.001; |
256 | |
|
257 | 0 | return t_strdup_printf("%d", score); |
258 | 0 | } |
259 | | |
260 | | int ext_spamvirustest_get_value(const struct sieve_runtime_env *renv, |
261 | | const struct sieve_extension *ext, bool percent, |
262 | | const char **value_r) |
263 | 0 | { |
264 | 0 | struct ext_spamvirustest_context *extctx = ext->context; |
265 | 0 | struct ext_spamvirustest_header_spec *status_header, *max_header; |
266 | 0 | struct sieve_message_context *msgctx = renv->msgctx; |
267 | 0 | struct ext_spamvirustest_message_context *mctx; |
268 | 0 | struct mail *mail; |
269 | 0 | ARRAY_TYPE(const_string) match_values; |
270 | 0 | const char *header_value, *error; |
271 | 0 | const char *status = NULL, *max = NULL; |
272 | 0 | float status_value, max_value; |
273 | 0 | unsigned int i, max_text; |
274 | 0 | pool_t pool = sieve_interpreter_pool(renv->interp); |
275 | |
|
276 | 0 | *value_r = "0"; |
277 | | |
278 | | /* |
279 | | * Check whether extension is properly configured |
280 | | */ |
281 | |
|
282 | 0 | if (extctx == NULL) { |
283 | 0 | sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS, |
284 | 0 | "error: extension not configured"); |
285 | 0 | return SIEVE_EXEC_OK; |
286 | 0 | } |
287 | | |
288 | | /* |
289 | | * Check wether a cached result is available |
290 | | */ |
291 | | |
292 | 0 | mctx = (struct ext_spamvirustest_message_context *) |
293 | 0 | sieve_message_context_extension_get(msgctx, ext); |
294 | |
|
295 | 0 | if (mctx == NULL) { |
296 | | /* Create new context */ |
297 | 0 | mctx = p_new(pool, struct ext_spamvirustest_message_context, 1); |
298 | 0 | sieve_message_context_extension_set(msgctx, ext, mctx); |
299 | 0 | } else if (mctx->reload_id == extctx->reload_id) { |
300 | | /* Use cached result */ |
301 | 0 | *value_r = ext_spamvirustest_get_score(ext, mctx->score_ratio, |
302 | 0 | percent); |
303 | 0 | return SIEVE_EXEC_OK; |
304 | 0 | } else { |
305 | | /* Extension was reloaded (probably in testsuite) */ |
306 | 0 | } |
307 | | |
308 | 0 | mctx->reload_id = extctx->reload_id; |
309 | | |
310 | | /* |
311 | | * Get max status value |
312 | | */ |
313 | |
|
314 | 0 | mail = sieve_message_get_mail(renv->msgctx); |
315 | 0 | status_header = &extctx->status_header; |
316 | 0 | max_header = &extctx->score_max_header; |
317 | |
|
318 | 0 | if (extctx->set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) { |
319 | 0 | if (max_header->header_name != NULL) { |
320 | | /* Get header from message */ |
321 | 0 | if (mail_get_first_header_utf8( |
322 | 0 | mail, max_header->header_name, |
323 | 0 | &header_value) < 0) { |
324 | 0 | return sieve_runtime_mail_error ( |
325 | 0 | renv, mail, "%s test: " |
326 | 0 | "failed to read header field '%s'", |
327 | 0 | sieve_extension_name(ext), |
328 | 0 | max_header->header_name); |
329 | 0 | } |
330 | 0 | if (header_value == NULL) { |
331 | 0 | sieve_runtime_trace( |
332 | 0 | renv, SIEVE_TRLVL_TESTS, |
333 | 0 | "header '%s' not found in message", |
334 | 0 | max_header->header_name); |
335 | 0 | goto failed; |
336 | 0 | } |
337 | | |
338 | 0 | if (max_header->regexp_match) { |
339 | 0 | t_array_init(&match_values, 2); |
340 | 0 | int ret; |
341 | | /* Execute regex */ |
342 | 0 | if ((ret = dregex_code_match_groups(max_header->regexp, header_value, |
343 | 0 | &match_values, &error)) < 1) { |
344 | 0 | if (ret < 0) { |
345 | 0 | sieve_runtime_trace( |
346 | 0 | renv, SIEVE_TRLVL_TESTS, |
347 | 0 | "score_max_header regexp for header '%s' failed" |
348 | 0 | "on value '%s': %s", |
349 | 0 | max_header->header_name, |
350 | 0 | header_value, |
351 | 0 | error); |
352 | 0 | } else { |
353 | 0 | sieve_runtime_trace( |
354 | 0 | renv, SIEVE_TRLVL_TESTS, |
355 | 0 | "score_max_header regexp for header '%s' did not match " |
356 | 0 | "on value '%s'", |
357 | 0 | max_header->header_name, |
358 | 0 | header_value); |
359 | 0 | } |
360 | 0 | goto failed; |
361 | 0 | } |
362 | 0 | if (array_count(&match_values) < 2) { |
363 | 0 | sieve_runtime_trace( |
364 | 0 | renv, SIEVE_TRLVL_TESTS, |
365 | 0 | "regexp did not return match value " |
366 | 0 | "for string '%s'", |
367 | 0 | header_value); |
368 | 0 | goto failed; |
369 | 0 | } |
370 | 0 | max = array_idx_elem(&match_values, 1); |
371 | 0 | } else { |
372 | 0 | max = header_value; |
373 | 0 | } |
374 | | |
375 | 0 | if (!ext_spamvirustest_parse_decimal_value( |
376 | 0 | max, &max_value, &error)) { |
377 | 0 | sieve_runtime_trace( |
378 | 0 | renv, SIEVE_TRLVL_TESTS, |
379 | 0 | "failed to parse maximum value: %s", |
380 | 0 | error); |
381 | 0 | goto failed; |
382 | 0 | } |
383 | 0 | } else { |
384 | 0 | max_value = extctx->set->parsed.score_max_value; |
385 | 0 | } |
386 | | |
387 | 0 | if (max_value == 0) { |
388 | 0 | sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS, |
389 | 0 | "error: max value is 0"); |
390 | 0 | goto failed; |
391 | 0 | } |
392 | 0 | } else { |
393 | 0 | max_value = (sieve_extension_is(ext, virustest_extension) ? |
394 | 0 | 5 : 10); |
395 | 0 | } |
396 | | |
397 | | /* |
398 | | * Get status value |
399 | | */ |
400 | | |
401 | | /* Get header from message */ |
402 | 0 | if (mail_get_first_header_utf8(mail, status_header->header_name, |
403 | 0 | &header_value) < 0) { |
404 | 0 | return sieve_runtime_mail_error( |
405 | 0 | renv, mail, "%s test: failed to read header field '%s'", |
406 | 0 | sieve_extension_name(ext), status_header->header_name); |
407 | 0 | } |
408 | 0 | if (header_value == NULL) { |
409 | 0 | sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS, |
410 | 0 | "header '%s' not found in message", |
411 | 0 | status_header->header_name); |
412 | 0 | goto failed; |
413 | 0 | } |
414 | | |
415 | | /* Execute regex */ |
416 | 0 | if (status_header->regexp_match) { |
417 | 0 | int ret; |
418 | 0 | t_array_init(&match_values, 2); |
419 | 0 | if ((ret = dregex_code_match_groups(status_header->regexp, header_value, |
420 | 0 | &match_values, &error)) < 1) { |
421 | 0 | if (ret < 0) { |
422 | 0 | sieve_runtime_trace( |
423 | 0 | renv, SIEVE_TRLVL_TESTS, |
424 | 0 | "status_header regexp for header '%s' did failed on value '%s': %s", |
425 | 0 | status_header->header_name, header_value, error); |
426 | |
|
427 | 0 | } else { |
428 | 0 | sieve_runtime_trace( |
429 | 0 | renv, SIEVE_TRLVL_TESTS, |
430 | 0 | "status_header regexp for header '%s' did not match on value '%s'", |
431 | 0 | status_header->header_name, header_value); |
432 | 0 | } |
433 | 0 | goto failed; |
434 | 0 | } |
435 | | |
436 | 0 | if (array_count(&match_values) < 2) { |
437 | 0 | sieve_runtime_trace( |
438 | 0 | renv, SIEVE_TRLVL_TESTS, |
439 | 0 | "regexp did not return match value for string '%s'", |
440 | 0 | header_value); |
441 | 0 | goto failed; |
442 | 0 | } |
443 | 0 | status = array_idx_elem(&match_values, 1); |
444 | 0 | } else { |
445 | 0 | status = header_value; |
446 | 0 | } |
447 | | |
448 | 0 | switch (extctx->set->parsed.status_type) { |
449 | 0 | case EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE: |
450 | 0 | if (!ext_spamvirustest_parse_decimal_value( |
451 | 0 | status, &status_value, &error)) { |
452 | 0 | sieve_runtime_trace( |
453 | 0 | renv, SIEVE_TRLVL_TESTS, |
454 | 0 | "failed to parse status value '%s': %s", |
455 | 0 | status, error); |
456 | 0 | goto failed; |
457 | 0 | } |
458 | 0 | break; |
459 | 0 | case EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN: |
460 | 0 | if (!ext_spamvirustest_parse_strlen_value( |
461 | 0 | status, &status_value, &error)) { |
462 | 0 | sieve_runtime_trace( |
463 | 0 | renv, SIEVE_TRLVL_TESTS, |
464 | 0 | "failed to parse status value '%s': %s", |
465 | 0 | status, error); |
466 | 0 | goto failed; |
467 | 0 | } |
468 | 0 | break; |
469 | 0 | case EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT: |
470 | 0 | max_text = (sieve_extension_is(ext, virustest_extension) ? |
471 | 0 | 5 : 10); |
472 | 0 | status_value = 0; |
473 | |
|
474 | 0 | i = 0; |
475 | 0 | while (i <= max_text) { |
476 | 0 | if (extctx->set->parsed.text_values[i] != NULL && |
477 | 0 | strcmp(status, |
478 | 0 | extctx->set->parsed.text_values[i]) == 0) { |
479 | 0 | status_value = (float)i; |
480 | 0 | break; |
481 | 0 | } |
482 | 0 | i++; |
483 | 0 | } |
484 | |
|
485 | 0 | if (i > max_text) { |
486 | 0 | sieve_runtime_trace( |
487 | 0 | renv, SIEVE_TRLVL_TESTS, |
488 | 0 | "failed to match textstatus value '%s'", |
489 | 0 | status); |
490 | 0 | goto failed; |
491 | 0 | } |
492 | 0 | break; |
493 | 0 | default: |
494 | 0 | i_unreached(); |
495 | 0 | } |
496 | | |
497 | | /* Calculate value */ |
498 | 0 | if (status_value < 0) |
499 | 0 | mctx->score_ratio = 0; |
500 | 0 | else if (status_value > max_value) |
501 | 0 | mctx->score_ratio = 1; |
502 | 0 | else |
503 | 0 | mctx->score_ratio = (status_value / max_value); |
504 | |
|
505 | 0 | sieve_runtime_trace( |
506 | 0 | renv, SIEVE_TRLVL_TESTS, |
507 | 0 | "extracted score=%.3f, max=%.3f, ratio=%.0f %%", |
508 | 0 | status_value, max_value, mctx->score_ratio * 100); |
509 | |
|
510 | 0 | *value_r = ext_spamvirustest_get_score(ext, mctx->score_ratio, percent); |
511 | 0 | return SIEVE_EXEC_OK; |
512 | | |
513 | 0 | failed: |
514 | 0 | mctx->score_ratio = -1; |
515 | 0 | *value_r = "0"; |
516 | 0 | return SIEVE_EXEC_OK; |
517 | 0 | } |