29fe79e01a06c712ec145ecbd8239725215d2501
[apps/pfixtools.git] / strlist.c
1 /******************************************************************************/
2 /*          pfixtools: a collection of postfix related tools                  */
3 /*          ~~~~~~~~~                                                         */
4 /*  ________________________________________________________________________  */
5 /*                                                                            */
6 /*  Redistribution and use in source and binary forms, with or without        */
7 /*  modification, are permitted provided that the following conditions        */
8 /*  are met:                                                                  */
9 /*                                                                            */
10 /*  1. Redistributions of source code must retain the above copyright         */
11 /*     notice, this list of conditions and the following disclaimer.          */
12 /*  2. Redistributions in binary form must reproduce the above copyright      */
13 /*     notice, this list of conditions and the following disclaimer in the    */
14 /*     documentation and/or other materials provided with the distribution.   */
15 /*  3. The names of its contributors may not be used to endorse or promote    */
16 /*     products derived from this software without specific prior written     */
17 /*     permission.                                                            */
18 /*                                                                            */
19 /*  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND   */
20 /*  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE     */
21 /*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR        */
22 /*  PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS    */
23 /*  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR    */
24 /*  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF      */
25 /*  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS  */
26 /*  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN   */
27 /*  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)   */
28 /*  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF    */
29 /*  THE POSSIBILITY OF SUCH DAMAGE.                                           */
30 /******************************************************************************/
31
32 /*
33  * Copyright © 2008 Florent Bruneau
34  */
35
36 #include "filter.h"
37 #include "trie.h"
38 #include "file.h"
39 #include "str.h"
40 #include "rbl.h"
41 #include "policy_tokens.h"
42 #include "resources.h"
43
44 typedef struct strlist_local_t {
45     char     *filename;
46     trie_t   **db;
47     int      weight;
48     unsigned reverse     :1;
49     unsigned partial     :1;
50 } strlist_local_t;
51 ARRAY(strlist_local_t)
52
53 typedef struct strlist_resource_t {
54     off_t  size;
55     time_t mtime;
56     trie_t *trie1;
57     trie_t *trie2;
58 } strlist_resource_t;
59
60 typedef struct strlist_config_t {
61     A(strlist_local_t) locals;
62
63     A(char)     hosts;
64     A(int)      host_offsets;
65     A(int)      host_weights;
66
67     int soft_threshold;
68     int hard_threshold;
69
70     unsigned is_email         :1;
71     unsigned is_hostname      :1;
72
73     unsigned match_sender     :1;
74     unsigned match_recipient  :1;
75
76     unsigned match_helo       :1;
77     unsigned match_client     :1;
78     unsigned match_reverse    :1;
79 } strlist_config_t;
80
81 typedef struct strlist_async_data_t {
82     A(rbl_result_t) results;
83     int awaited;
84     uint32_t sum;
85     bool error;
86 } strlist_async_data_t;
87
88 static filter_type_t filter_type = FTK_UNKNOWN;
89
90
91 static void strlist_local_wipe(strlist_local_t *entry)
92 {
93     if (entry->filename != NULL) {
94         resource_release("strlist", entry->filename);
95         p_delete(&entry->filename);
96     }
97 }
98
99 static void strlist_resource_wipe(strlist_resource_t *res)
100 {
101     trie_delete(&res->trie1);
102     trie_delete(&res->trie2);
103     p_delete(&res);
104 }
105
106 static strlist_config_t *strlist_config_new(void)
107 {
108     return p_new(strlist_config_t, 1);
109 }
110
111 static void strlist_config_delete(strlist_config_t **config)
112 {
113     if (*config) {
114         array_deep_wipe((*config)->locals, strlist_local_wipe);
115         array_wipe((*config)->hosts);
116         array_wipe((*config)->host_offsets);
117         array_wipe((*config)->host_weights);
118         p_delete(config);
119     }
120 }
121
122 static inline void strlist_copy(char *dest, const char *str, ssize_t str_len,
123                                 bool reverse)
124 {
125     if (str_len > 0) {
126         if (reverse) {
127             for (const char *src = str + str_len - 1 ; src >= str ; --src) {
128                 *dest = ascii_tolower(*src);
129                 ++dest;
130             }
131         } else {
132             for (int i = 0 ; i < str_len ; ++i) {
133                 *dest = ascii_tolower(str[i]);
134                 ++dest;
135             }
136         }
137     }
138     *dest = '\0';
139 }
140
141
142 static bool strlist_create(strlist_local_t *local,
143                            const char *file, int weight,
144                            bool reverse, bool partial, bool lock)
145 {
146     file_map_t map;
147     const char *p, *end;
148     char line[BUFSIZ];
149     uint32_t count = 0;
150
151     if (!file_map_open(&map, file, false)) {
152         return false;
153     }
154     p   = map.map;
155     end = map.end;
156     while (end > p && end[-1] != '\n') {
157         --end;
158     }
159     if (end != map.end) {
160         warn("file %s miss a final \\n, ignoring last line",
161              file);
162     }
163
164     strlist_resource_t *res = resource_get("strlist", file);
165     if (res == NULL) {
166         res = p_new(strlist_resource_t, 1);
167         resource_set("strlist", file, res, (resource_destructor_t)strlist_resource_wipe);
168     } else if (res->trie2 != NULL) {
169         err("A file (%s) cannot be used as a rbldns zone file and a strlist file at the same time",
170             file);
171         resource_release("strlist", file);
172         file_map_close(&map);
173         return false;
174     }
175
176     p_clear(local, 1);
177     local->filename = m_strdup(file);
178     local->db      = &res->trie1;
179     local->weight  = weight;
180     local->reverse = reverse;
181     local->partial = partial;
182     if (res->size == map.st.st_size && res->mtime == map.st.st_mtime) {
183         info("strlist %s up to date", file);
184         file_map_close(&map);
185         return true;
186     }
187     trie_delete(&res->trie1);
188     res->trie1 = trie_new();
189     res->size  = map.st.st_size;
190     res->mtime = map.st.st_mtime;
191
192     while (p < end && p != NULL) {
193         const char *eol = (char *)memchr(p, '\n', end - p);
194         if (eol == NULL) {
195             eol = end;
196         }
197         if (eol - p >= BUFSIZ) {
198             err("unreasonnable long line");
199             file_map_close(&map);
200             trie_delete(&res->trie1);
201             strlist_local_wipe(local);
202             return false;
203         }
204         if (*p != '#') {
205             const char *eos = eol;
206             while (p < eos && isspace(*p)) {
207                 ++p;
208             }
209             while (p < eos && isspace(eos[-1])) {
210                 --eos;
211             }
212             if (p < eos) {
213                 strlist_copy(line, p, eos - p, reverse);
214                 trie_insert(res->trie1, line);
215                 ++count;
216             }
217         }
218         p = eol + 1;
219     }
220     file_map_close(&map);
221     trie_compile(res->trie1, lock);
222     info("%s loaded, %u entries", file, count);
223     return true;
224 }
225
226 static bool strlist_create_from_rhbl(strlist_local_t *hosts, strlist_local_t *domains,
227                                      const char *file, int weight, bool lock)
228 {
229     uint32_t host_count, domain_count;
230     file_map_t map;
231     const char *p, *end;
232     char line[BUFSIZ];
233
234     if (!file_map_open(&map, file, false)) {
235         return false;
236     }
237     p   = map.map;
238     end = map.end;
239     while (end > p && end[-1] != '\n') {
240         --end;
241     }
242     if (end != map.end) {
243         warn("file %s miss a final \\n, ignoring last line",
244              file);
245     }
246
247
248     strlist_resource_t *res = resource_get("strlist", file);
249     if (res == NULL) {
250         res = p_new(strlist_resource_t, 1);
251         resource_set("strlist", file, res, (resource_destructor_t)strlist_resource_wipe);
252     } else if (res->trie2 == NULL) {
253         err("A file (%s) cannot be used as a rbldns zone file and a strlist file at the same time",
254             file);
255         resource_release("strlist", file);
256         file_map_close(&map);
257         return false;
258     }
259
260     p_clear(hosts, 1);
261     hosts->filename = m_strdup(file);
262     hosts->db = &res->trie1;
263     hosts->weight = weight;
264     hosts->reverse    = true;
265     host_count = 0;
266
267     p_clear(domains, 1);
268     /* don't set filename */
269     domains->db = &res->trie2;
270     domains->weight = weight;
271     domains->reverse      = true;
272     domains->partial      = true;
273     domain_count = 0;
274
275     if (map.st.st_size == res->size && map.st.st_mtime == res->mtime) {
276         info("rbldns %s up to date", file);
277         file_map_close(&map);
278         return true;
279     }
280
281     trie_delete(&res->trie1);
282     trie_delete(&res->trie2);
283     res->trie1 = trie_new();
284     res->trie2 = trie_new();
285     res->size  = map.st.st_size;
286     res->mtime = map.st.st_mtime;
287
288     while (p < end && p != NULL) {
289         const char *eol = (char *)memchr(p, '\n', end - p);
290         if (eol == NULL) {
291             eol = end;
292         }
293         if (eol - p >= BUFSIZ) {
294             err("unreasonnable long line");
295             file_map_close(&map);
296             trie_delete(&res->trie1);
297             trie_delete(&res->trie2);
298             strlist_local_wipe(hosts);
299             return false;
300         }
301         if (*p != '#') {
302             const char *eos = eol;
303             while (p < eos && isspace(*p)) {
304                 ++p;
305             }
306             while (p < eos && isspace(eos[-1])) {
307                 --eos;
308             }
309             if (p < eos) {
310                 if (isalnum(*p)) {
311                     strlist_copy(line, p, eos - p, true);
312                     trie_insert(res->trie1, line);
313                     ++host_count;
314                 } else if (*p == '*') {
315                     ++p;
316                     strlist_copy(line, p, eos - p, true);
317                     trie_insert(res->trie2, line);
318                     ++domain_count;
319                 }
320             }
321         }
322         p = eol + 1;
323     }
324     file_map_close(&map);
325     if (host_count > 0) {
326         trie_compile(res->trie1, lock);
327     } else {
328         trie_delete(&res->trie1);
329     }
330     if (domain_count > 0) {
331         trie_compile(res->trie2, lock);
332     } else {
333         trie_delete(&res->trie2);
334     }
335     info("rhbl %s loaded, %u hosts, %u domains", file, host_count, domain_count);
336     if (res->trie1 == NULL && res->trie2 == NULL) {
337         strlist_local_wipe(hosts);
338         return false;
339     }
340     return true;
341 }
342
343
344 static bool strlist_filter_constructor(filter_t *filter)
345 {
346     strlist_config_t *config = strlist_config_new();
347
348 #define PARSE_CHECK(Expr, Str, ...)                                            \
349     if (!(Expr)) {                                                             \
350         err(Str, ##__VA_ARGS__);                                               \
351         strlist_config_delete(&config);                                        \
352         return false;                                                          \
353     }
354
355     config->hard_threshold = 1;
356     config->soft_threshold = 1;
357     foreach (filter_param_t *param, filter->params) {
358         switch (param->type) {
359           /* file parameter is:
360            *  [no]lock:(partial-)(prefix|suffix):weight:filename
361            *  valid options are:
362            *    - lock:   memlock the database in memory.
363            *    - nolock: don't memlock the database in memory.
364            *    - prefix: perform "prefix" compression on storage.
365            *    - suffix  perform "suffix" compression on storage.
366            *    - \d+:    a number describing the weight to give to the match
367            *              the given list [mandatory]
368            *  the file pointed by filename MUST be a valid string list (one string per
369            *  line, empty lines and lines beginning with a '#' are ignored).
370            */
371           case ATK_FILE: {
372             bool lock = false;
373             int  weight = 0;
374             bool reverse = false;
375             bool partial = false;
376             const char *current = param->value;
377             const char *p = m_strchrnul(param->value, ':');
378             char *next = NULL;
379             for (int i = 0 ; i < 4 ; ++i) {
380                 PARSE_CHECK(i == 3 || *p,
381                             "file parameter must contains a locking state "
382                             "and a weight option");
383                 switch (i) {
384                   case 0:
385                     if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
386                         lock = true;
387                     } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
388                         lock = false;
389                     } else {
390                         PARSE_CHECK(false, "illegal locking state %.*s",
391                                     (int)(p - current), current);
392                     }
393                     break;
394
395                   case 1:
396                     if (p - current > (ssize_t)strlen("partial-") 
397                         && strncmp(current, "partial-", strlen("partial-")) == 0) {
398                         partial = true;
399                         current += strlen("partial-");
400                     }
401                     if ((p - current) == 6 && strncmp(current, "suffix", 6) == 0) {
402                         reverse = true;
403                     } else if ((p - current) == 6 && strncmp(current, "prefix", 6) == 0) {
404                         reverse = false;
405                     } else {
406                         PARSE_CHECK(false, "illegal character order value %.*s",
407                                     (int)(p - current), current);
408                     }
409                     break;
410
411                   case 2:
412                     weight = strtol(current, &next, 10);
413                     PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
414                                 "illegal weight value %.*s",
415                                 (int)(p - current), current);
416                     break;
417
418                   case 3: {
419                     strlist_local_t entry;
420                     PARSE_CHECK(strlist_create(&entry, current, weight,
421                                                reverse, partial, lock),
422                                 "cannot load string list from %s", current);
423                     array_add(config->locals, entry);
424                   } break;
425                 }
426                 if (i != 3) {
427                     current = p + 1;
428                     p = m_strchrnul(current, ':');
429                 }
430             }
431           } break;
432
433           /* rbldns parameter is:
434            *  [no]lock::weight:filename
435            *  valid options are:
436            *    - lock:   memlock the database in memory.
437            *    - nolock: don't memlock the database in memory.
438            *    - \d+:    a number describing the weight to give to the match
439            *              the given list [mandatory]
440            *  directly import a file issued from a rhbl in rbldns format.
441            */
442           case ATK_RBLDNS: {
443             bool lock = false;
444             int  weight = 0;
445             const char *current = param->value;
446             const char *p = m_strchrnul(param->value, ':');
447             char *next = NULL;
448             for (int i = 0 ; i < 3 ; ++i) {
449                 PARSE_CHECK(i == 2 || *p,
450                             "file parameter must contains a locking state "
451                             "and a weight option");
452                 switch (i) {
453                   case 0:
454                     if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
455                         lock = true;
456                     } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
457                         lock = false;
458                     } else {
459                         PARSE_CHECK(false, "illegal locking state %.*s",
460                                     (int)(p - current), current);
461                     }
462                     break;
463
464                   case 1:
465                     weight = strtol(current, &next, 10);
466                     PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
467                                 "illegal weight value %.*s",
468                                 (int)(p - current), current);
469                     break;
470
471                   case 2: {
472                     strlist_local_t trie_hosts, trie_domains;
473                     PARSE_CHECK(strlist_create_from_rhbl(&trie_hosts, &trie_domains,
474                                                          current, weight, lock),
475                                 "cannot load string list from rhbl %s", current);
476                     if (trie_hosts.db != NULL) {
477                         array_add(config->locals, trie_hosts);
478                     }
479                     if (trie_domains.db != NULL) {
480                         array_add(config->locals, trie_domains);
481                     }
482                     config->is_hostname = true;
483                   } break;
484                 }
485                 if (i != 2) {
486                     current = p + 1;
487                     p = m_strchrnul(current, ':');
488                 }
489             }
490           } break;
491
492           /* dns parameter.
493            *  weight:hostname.
494            * define a RBL to use through DNS resolution.
495            */
496           case ATK_DNS: {
497             int  weight = 0;
498             const char *current = param->value;
499             const char *p = m_strchrnul(param->value, ':');
500             char *next = NULL;
501             for (int i = 0 ; i < 2 ; ++i) {
502                 PARSE_CHECK(i == 1 || *p,
503                             "host parameter must contains a weight option");
504                 switch (i) {
505                   case 0:
506                     weight = strtol(current, &next, 10);
507                     PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
508                                 "illegal weight value %.*s",
509                                 (int)(p - current), current);
510                     break;
511
512                   case 1:
513                     array_add(config->host_offsets, array_len(config->hosts));
514                     array_append(config->hosts, current, strlen(current) + 1);
515                     array_add(config->host_weights, weight);
516                     break;
517                 }
518                 if (i != 1) {
519                     current = p + 1;
520                     p = m_strchrnul(current, ':');
521                 }
522             }
523           } break;
524
525           /* hard_threshold parameter is an integer.
526            *  If the matching score is greater or equal than this threshold,
527            *  the hook "hard_match" is called.
528            * hard_threshold = 1 means, that all matches are hard matches.
529            * default is 1;
530            */
531           FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, config->hard_threshold);
532
533           /* soft_threshold parameter is an integer.
534            *  if the matching score is greater or equal than this threshold
535            *  and smaller or equal than the hard_threshold, the hook "soft_match"
536            *  is called.
537            * default is 1;
538            */
539           FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, config->soft_threshold);
540
541           /* fields to match againes:
542            *  fields = field_name(,field_name)*
543            *  field_names are
544            *    - hostname: helo_name,client_name,reverse_client_name
545            *    - email: sender,recipient
546            */
547           case ATK_FIELDS: {
548             const char *current = param->value;
549             const char *p = m_strchrnul(param->value, ',');
550             do {
551                 postlicyd_token tok = policy_tokenize(current, p - current);
552                 switch (tok) {
553 #define           CASE(Up, Low, Type)                                          \
554                   case PTK_ ## Up:                                             \
555                     config->match_ ## Low = true;                              \
556                     config->is_ ## Type = true;                                \
557                     break
558                   CASE(HELO_NAME, helo, hostname);
559                   CASE(CLIENT_NAME, client, hostname);
560                   CASE(REVERSE_CLIENT_NAME, reverse, hostname);
561                   CASE(SENDER_DOMAIN, sender, hostname);
562                   CASE(RECIPIENT_DOMAIN, recipient, hostname);
563                   CASE(SENDER, sender, email);
564                   CASE(RECIPIENT, recipient, email);
565 #undef CASE
566                   default:
567                     PARSE_CHECK(false, "unknown field %.*s", (int)(p - current), current);
568                     break;
569                 }
570                 if (!*p) {
571                     break;
572                 }
573                 current = p + 1;
574                 p = m_strchrnul(current, ',');
575             } while (true);
576           } break;
577
578           default: break;
579         }
580     }}
581
582     PARSE_CHECK(config->is_email != config->is_hostname,
583                 "matched field MUST be emails XOR hostnames");
584     PARSE_CHECK(config->locals.len || config->host_offsets.len,
585                 "no file parameter in the filter %s", filter->name);
586     filter->data = config;
587     return true;
588 }
589
590 static void strlist_filter_destructor(filter_t *filter)
591 {
592     strlist_config_t *config = filter->data;
593     strlist_config_delete(&config);
594     filter->data = config;
595 }
596
597 static void strlist_filter_async(rbl_result_t *result, void *arg)
598 {
599     filter_context_t   *context = arg;
600     const filter_t      *filter = context->current_filter;
601     const strlist_config_t *data = filter->data;
602     strlist_async_data_t  *async = context->contexts[filter_type];
603
604     if (*result != RBL_ERROR) {
605         async->error = false;
606     }
607     --async->awaited;
608
609     debug("got asynchronous request result for filter %s, rbl %d, still awaiting %d answers",
610           filter->name, (int)(result - array_ptr(async->results, 0)), async->awaited);
611
612     if (async->awaited == 0) {
613         filter_result_t res = HTK_FAIL;
614         if (async->error) {
615             res = HTK_ERROR;
616         } else {
617             uint32_t j = 0;
618 #define DO_SUM(Field)                                                          \
619         if (data->match_ ## Field) {                                           \
620             for (uint32_t i = 0 ; i < array_len(data->host_offsets) ; ++i) {   \
621                 int weight = array_elt(data->host_weights, i);                 \
622                                                                                \
623                 switch (array_elt(async->results, j)) {                        \
624                   case RBL_ASYNC:                                              \
625                     crit("no more awaited answer but result is ASYNC");        \
626                     abort();                                                   \
627                   case RBL_FOUND:                                              \
628                     async->sum += weight;                                      \
629                     break;                                                     \
630                   default:                                                     \
631                     break;                                                     \
632                 }                                                              \
633                 ++j;                                                           \
634             }                                                                  \
635         }
636             DO_SUM(helo);
637             DO_SUM(client);
638             DO_SUM(reverse);
639             DO_SUM(recipient);
640             DO_SUM(sender);
641 #undef DO_SUM
642             debug("score is %d", async->sum);
643             if (async->sum >= (uint32_t)data->hard_threshold) {
644                 res = HTK_HARD_MATCH;
645             } else if (async->sum >= (uint32_t)data->soft_threshold) {
646                 res = HTK_SOFT_MATCH;
647             }
648         }
649         debug("answering to filter %s", filter->name);
650         filter_post_async_result(context, res);
651     }
652 }
653
654
655 static filter_result_t strlist_filter(const filter_t *filter, const query_t *query,
656                                       filter_context_t *context)
657 {
658     char reverse[BUFSIZ];
659     char normal[BUFSIZ];
660     const strlist_config_t *config = filter->data;
661     strlist_async_data_t *async = context->contexts[filter_type];
662     int result_pos = 0;
663     async->sum = 0;
664     async->error = true;
665     array_ensure_exact_capacity(async->results, (config->match_client
666                                 + config->match_sender + config->match_helo
667                                 + config->match_recipient + config->match_reverse)
668                                 * array_len(config->host_offsets));
669     async->awaited = 0;
670
671
672     if (config->is_email && 
673         ((config->match_sender && query->state < SMTP_MAIL)
674         || (config->match_recipient && query->state != SMTP_RCPT))) {
675         warn("trying to match an email against a field that is not "
676              "available in current protocol state");
677         return HTK_ABORT;
678     } else if (config->is_hostname && config->match_helo && query->state < SMTP_HELO) {
679         warn("trying to match hostname against helo before helo is received");
680         return HTK_ABORT;
681     }
682 #define LOOKUP(Flag, Field)                                                    \
683     if (config->match_ ## Flag) {                                              \
684         const int len = m_strlen(query->Field);                                \
685         strlist_copy(normal, query->Field, len, false);                        \
686         strlist_copy(reverse, query->Field, len, true);                        \
687         foreach (strlist_local_t *entry, config->locals) {                     \
688             if ((!entry->partial && trie_lookup(*(entry->db),                  \
689                                       entry->reverse ? reverse : normal))      \
690                 || (entry->partial && trie_prefix(*(entry->db),                \
691                                                   entry->reverse ? reverse : normal))) {      \
692                 async->sum += entry->weight;                                   \
693                 if (async->sum >= (uint32_t)config->hard_threshold) {          \
694                     return HTK_HARD_MATCH;                                     \
695                 }                                                              \
696             }                                                                  \
697             async->error = false;                                              \
698         }}                                                                     \
699     }
700 #define DNS(Flag, Field)                                                       \
701     if (config->match_ ## Flag) {                                              \
702         const int len = m_strlen(query->Field);                                \
703         strlist_copy(normal, query->Field, len, false);                        \
704         for (uint32_t i = 0 ; len > 0 && i < config->host_offsets.len ; ++i) { \
705             const char *rbl = array_ptr(config->hosts,                         \
706                                         array_elt(config->host_offsets, i));   \
707             debug("running check of field %s (%s) against %s", STR(Field),     \
708                   normal, rbl);                                                \
709             if (rhbl_check(rbl, normal, array_ptr(async->results, result_pos), \
710                            strlist_filter_async, context)) {                   \
711                 async->error = false;                                          \
712                 ++async->awaited;                                              \
713             }                                                                  \
714             ++result_pos;                                                      \
715         }                                                                      \
716     }
717
718     if (config->is_email) {
719         LOOKUP(sender, sender);
720         LOOKUP(recipient, recipient);
721         DNS(sender, sender);
722         DNS(recipient, recipient);
723     } else if (config->is_hostname) {
724         LOOKUP(helo, helo_name);
725         LOOKUP(client, client_name);
726         LOOKUP(reverse, reverse_client_name);
727         LOOKUP(recipient, recipient_domain);
728         LOOKUP(sender, sender_domain);
729         DNS(helo, helo_name);
730         DNS(client, client_name);
731         DNS(reverse, reverse_client_name);
732         DNS(recipient, recipient_domain);
733         DNS(sender, sender_domain);
734     }
735 #undef  DNS
736 #undef  LOOKUP
737     if (async->awaited > 0) {
738         return HTK_ASYNC;
739     }
740     if (async->error) {
741         err("filter %s: all the rbls returned an error", filter->name);
742         return HTK_ERROR;
743     }
744     if (async->sum >= (uint32_t)config->hard_threshold) {
745         return HTK_HARD_MATCH;
746     } else if (async->sum >= (uint32_t)config->soft_threshold) {
747         return HTK_SOFT_MATCH;
748     } else {
749         return HTK_FAIL;
750     }
751 }
752
753 static void *strlist_context_constructor(void)
754 {
755     return p_new(strlist_async_data_t, 1);
756 }
757
758 static void strlist_context_destructor(void *data)
759 {
760     strlist_async_data_t *ctx = data;
761     array_wipe(ctx->results);
762     p_delete(&ctx);
763 }
764
765 static int strlist_init(void)
766 {
767     filter_type =  filter_register("strlist", strlist_filter_constructor,
768                                    strlist_filter_destructor, strlist_filter,
769                                    strlist_context_constructor,
770                                    strlist_context_destructor);
771     /* Hooks.
772      */
773     (void)filter_hook_register(filter_type, "abort");
774     (void)filter_hook_register(filter_type, "error");
775     (void)filter_hook_register(filter_type, "fail");
776     (void)filter_hook_register(filter_type, "hard_match");
777     (void)filter_hook_register(filter_type, "soft_match");
778
779     /* Parameters.
780      */
781     (void)filter_param_register(filter_type, "file");
782     (void)filter_param_register(filter_type, "rbldns");
783     (void)filter_param_register(filter_type, "dns");
784     (void)filter_param_register(filter_type, "hard_threshold");
785     (void)filter_param_register(filter_type, "soft_threshold");
786     (void)filter_param_register(filter_type, "fields");
787     return 0;
788 }
789 module_init(strlist_init);