Merge branch 'master' into with-dns-bl
authorFlorent Bruneau <florent.bruneau@polytechnique.org>
Sun, 5 Oct 2008 13:22:54 +0000 (15:22 +0200)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Sun, 5 Oct 2008 13:22:54 +0000 (15:22 +0200)
Conflicts:

postlicyd/iplist.c
postlicyd/tst-rbl.c

Signed-off-by: Florent Bruneau <florent.bruneau@polytechnique.org>
13 files changed:
common/Makefile
common/array.h
common/common.c
common/tst-trie.c
example/postlicyd.conf
pfix-srsd/Makefile
postlicyd/Makefile
postlicyd/iplist.c
postlicyd/main-postlicyd.c
postlicyd/strlist.c
postlicyd/tst-rbl.c
tools/postgrey2postlicyd [new file with mode: 0755]
tools/rbldns2postlicyd [new file with mode: 0755]

index be9d473..3f30e73 100644 (file)
@@ -35,4 +35,6 @@ TESTS = tst-trie
 lib_SOURCES = str.c buffer.c common.c epoll.c server.c trie.c file.c rbl.c
 tst-trie_SOURCES = tst-trie.c lib.a
 
+all:
+
 include ../mk/common.mk
index cd0090a..6f5217d 100644 (file)
 ARRAY(char)
 ARRAY(int)
 ARRAY(bool)
+ARRAY(uint16_t)
 ARRAY(uint32_t)
 
 PARRAY(void)
index af95efa..3d2862f 100644 (file)
@@ -52,7 +52,7 @@ static FILE *pidfile = NULL;
 void common_sighandler(int sig)
 {
     switch (sig) {
-                       case SIGTERM:
+      case SIGTERM:
       case SIGINT:
         sigint = true;
         return;
@@ -62,7 +62,7 @@ void common_sighandler(int sig)
         return;
 
       default:
-                               err("Killed (got signal %d)...", sig);
+        err("Killed (got signal %d)...", sig);
         exit(-1);
     }
 }
@@ -199,11 +199,11 @@ int daemon_detach(void)
     pid = fork();
     if (pid < 0) {
         return -1;
-               }
+    }
     if (pid) {
-                               daemon_process = false;
+        daemon_process = false;
         exit(0);
-               }
+    }
 
     setsid();
     return 0;
@@ -236,20 +236,11 @@ int drop_privileges(const char *user, const char *group)
 
 int pidfile_open(const char *name)
 {
-               struct flock lock;
-               p_clear(&lock, 1);
-               lock.l_type = F_WRLCK;
     if (name) {
         pidfile = fopen(name, "w");
         if (!pidfile)
             return -1;
-                               if (fcntl(fileno(pidfile), F_SETLK, &lock) == -1) {
-                                               crit("program already started");
-                                               fclose(pidfile);
-                                               pidfile = NULL;
-                                               return -1;
-                               }
-                               fprintf(pidfile, "%d\n", getpid());
+        fprintf(pidfile, "%d\n", getpid());
         return fflush(pidfile);
     }
     return 0;
@@ -268,14 +259,12 @@ int pidfile_refresh(void)
 
 static void pidfile_close(void)
 {
-               struct flock lock;
-               p_clear(&lock, 1);
-               lock.l_type = F_UNLCK;
     if (pidfile) {
-        rewind(pidfile);
-        ftruncate(fileno(pidfile), 0);
-        fcntl(fileno(pidfile), F_SETLK, &lock);
-                               fclose(pidfile);
+        if (daemon_process) {
+            rewind(pidfile);
+            ftruncate(fileno(pidfile), 0);
+        }
+        fclose(pidfile);
         pidfile = NULL;
     }
 }
@@ -283,6 +272,11 @@ static void pidfile_close(void)
 int common_setup(const char* pidfilename, bool unsafe, const char* runas_user,
                  const char* runas_group, bool daemonize)
 {
+    if (pidfile_open(pidfilename) < 0) {
+        crit("unable to write pidfile %s", pidfilename);
+        return EXIT_FAILURE;
+    }
+
     if (!unsafe && drop_privileges(runas_user, runas_group) < 0) {
         crit("unable to drop privileges");
         return EXIT_FAILURE;
@@ -293,11 +287,6 @@ int common_setup(const char* pidfilename, bool unsafe, const char* runas_user,
         return EXIT_FAILURE;
     }
 
-               if (pidfile_open(pidfilename) < 0) {
-        crit("unable to write pidfile %s", pidfilename);
-        return EXIT_FAILURE;
-    }
-
     pidfile_refresh();
     return EXIT_SUCCESS;
 }
@@ -307,10 +296,10 @@ extern exitcall_t __madexit[];
 
 static void common_shutdown(void)
 {
-               if (daemon_process) {
-                               info("stopping...");
-               }
-               pidfile_close();
+    if (daemon_process) {
+        info("stopping...");
+    }
+    pidfile_close();
     for (int i = -1; __madexit[i]; i--) {
         (*__madexit[i])();
     }
index c9e16dc..9f69b1e 100644 (file)
@@ -128,7 +128,7 @@ int main(int argc, char *argv[])
      */
     if (argc > 1) {
         trie = create_trie_from_file(argv[1]);
-        trie_inspect(trie, true);
+        trie_inspect(trie, false);
         trie_delete(&trie);
     }
     return 0;
index 123c66a..12d8ff5 100644 (file)
@@ -70,6 +70,8 @@
 #             declare a file to load. If lock is given, the klist is locked into the
 #             RAM. The weight is a number giving the weight of this blaclist file in the
 #             score of the IP
+#           - rbldns: (no)?lock:weight:filename
+#             this is an alias for file.
 #           - soft_threshold: score (default: 1)
 #             minimum score to match the soft_match return value
 #           - hard_threshold: score (default: 1)
@@ -117,6 +119,12 @@ spamhaus_and_abuseat {
 #                * a file that contains "postmaster@" in "partial-prefix" mode will match all
 #                 postmaster emails.
 #                * a file open without "partial-" modifier match exact strings.
+#           - rbldns: (no)?lock:weight:filename
+#             declare a rbldns zone file to load. This is exactly the same as file excepted that it wraps
+#             parsing of hostname to split them into 2 categories:
+#               * names beginning with '*' are sorted as 'domains' and are matched as suffix
+#               * names starting with an alphanumirical character are sorted as 'hostnames' and are
+#                process via exact matching.
 #           - soft_threshold: score (default: 1)
 #             minimum score to match the soft_match return value
 #           - hard_threshold: score (default: 1)
@@ -149,7 +157,8 @@ client_whitelist {
 
   # configuration
   file    = lock:1:suffix:/var/spool/postlicyd/client_whitelist;
-  fields  = client_name;
+  rbldns  = lock:1:/va/spool/postlicyd/abuse.rfc-ignorant.org;
+  fields  = client_name,sender_domain,helo_name;
 
   # hooks
   on_hard_match = postfix:OK;
index 65f3f36..0be9e82 100644 (file)
@@ -34,4 +34,6 @@ PROGRAMS  = pfix-srsd
 pfix-srsd_SOURCES = main-srsd.c ../common/lib.a
 pfix-srsd_LIBADD  = -lsrs2
 
+all:
+
 include ../mk/common.mk
index 84dec14..0983841 100644 (file)
@@ -36,18 +36,20 @@ GENERATED = policy_tokens.h policy_tokens.c \
                                                filter_tokens.h filter_tokens.c \
                                                hook_tokens.h hook_tokens.c \
                                                param_tokens.h param_tokens.c
-TESTS     = test-rbl tst-filters
+TESTS     = tst-rbl tst-filters
 
 FILTERS                = iplist.c greylist.c strlist.c match.c
 
 postlicyd_SOURCES = main-postlicyd.c ../common/lib.a filter.c config.c query.c $(FILTERS) $(GENERATED)
 postlicyd_LIBADD  = $(TC_LIBS)
 
-tst-rbl_SOURCES   = tst-rbl.c
+tst-rbl_SOURCES   = tst-rbl.c ../common/lib.a filter.c config.c query.c iplist.c $(GENERATED)
 tst-filters_SOURCES = tst-filters.c ../common/lib.a config.c filter.c query.c $(FILTERS) $(GENERATED)
 tst-filters_LIBADD  = $(TC_LIBS)
 
 hook_tokens.h hook_tokens.c: $(FILTERS)
 param_tokens.c param_tokens.h: $(FILTERS) config.c
 
+all:
+
 include ../mk/common.mk
index 256c360..3c14fda 100644 (file)
@@ -62,7 +62,7 @@ enum {
 };
 
 struct rbldb_t {
-    A(uint32_t) ips;
+    A(uint16_t) ips[1 << 16];
 };
 ARRAY(rbldb_t)
 
@@ -123,6 +123,7 @@ rbldb_t *rbldb_create(const char *file, bool lock)
     rbldb_t *db;
     file_map_t map;
     const char *p, *end;
+    uint32_t ips = 0;
 
     if (!file_map_open(&map, file, false)) {
         return NULL;
@@ -148,33 +149,37 @@ rbldb_t *rbldb_create(const char *file, bool lock)
         if (parse_ipv4(p, &p, &ip) < 0) {
             p = (char *)memchr(p, '\n', end - p) + 1;
         } else {
-            array_add(db->ips, ip);
+            array_add(db->ips[ip >> 16], ip & 0xffff);
+            ++ips;
         }
     }
     file_map_close(&map);
 
     /* Lookup may perform serveral I/O, so avoid swap.
      */
-    array_adjust(db->ips);
-    if (lock && !array_lock(db->ips)) {
-        UNIXERR("mlock");
-    }
-
-    if (db->ips.len) {
-#       define QSORT_TYPE uint32_t
-#       define QSORT_BASE db->ips.data
-#       define QSORT_NELT db->ips.len
+    for (int i = 0 ; i < 1 << 16 ; ++i) {
+        array_adjust(db->ips[i]);
+        if (lock && !array_lock(db->ips[i])) {
+            UNIXERR("mlock");
+        }
+        if (db->ips[i].len) {
+#       define QSORT_TYPE uint16_t
+#       define QSORT_BASE db->ips[i].data
+#       define QSORT_NELT db->ips[i].len
 #       define QSORT_LT(a,b) *a < *b
 #       include "qsort.c"
+        }
     }
 
-    info("rbl %s loaded, %d IPs", file, db->ips.len);
+    info("rbl %s loaded, %d IPs", file, ips);
     return db;
 }
 
 static void rbldb_wipe(rbldb_t *db)
 {
-    array_wipe(db->ips);
+    for (int i = 0 ; i < 1 << 16 ; ++i) {
+        array_wipe(db->ips[i]);
+    }
 }
 
 void rbldb_delete(rbldb_t **db)
@@ -187,20 +192,26 @@ void rbldb_delete(rbldb_t **db)
 
 uint32_t rbldb_stats(const rbldb_t *rbl)
 {
-    return rbl->ips.len;
+    uint32_t ips = 0;
+    for (int i = 0 ; i < 1 << 16 ; ++i) {
+        ips += array_len(rbl->ips[i]);
+    }
+    return ips;
 }
 
 bool rbldb_ipv4_lookup(const rbldb_t *db, uint32_t ip)
 {
-    int l = 0, r = db->ips.len;
+    const uint16_t hip = ip >> 16;
+    const uint16_t lip = ip & 0xffff;
+    int l = 0, r = db->ips[hip].len;
 
     while (l < r) {
         int i = (r + l) / 2;
 
-        if (array_elt(db->ips, i) == ip)
+        if (array_elt(db->ips[hip], i) == lip)
             return true;
 
-        if (ip < array_elt(db->ips, i)) {
+        if (lip < array_elt(db->ips[hip], i)) {
             r = i;
         } else {
             l = i + 1;
@@ -268,7 +279,7 @@ static bool rbl_filter_constructor(filter_t *filter)
            *  the file pointed by filename MUST be a valid ip list issued from
            *  the rsync (or equivalent) service of a (r)bl.
            */
-          case ATK_FILE: {
+          case ATK_FILE: case ATK_RBLDNS: {
             bool lock = false;
             int  weight = 0;
             rbldb_t *rbl = NULL;
@@ -314,11 +325,11 @@ static bool rbl_filter_constructor(filter_t *filter)
             }
           } break;
 
-          /* host parameter.
+          /* dns parameter.
            *  weight:hostname.
            * define a RBL to use through DNS resolution.
            */
-          case ATK_HOST: {
+          case ATK_DNS: {
             int  weight = 0;
             const char *current = param->value;
             const char *p = m_strchrnul(param->value, ':');
@@ -451,7 +462,8 @@ static int rbl_init(void)
     /* Parameters.
      */
     (void)filter_param_register(type, "file");
-    (void)filter_param_register(type, "host");
+    (void)filter_param_register(type, "rbldns");
+    (void)filter_param_register(type, "dns");
     (void)filter_param_register(type, "hard_threshold");
     (void)filter_param_register(type, "soft_threshold");
     return 0;
index 1f286eb..9ff1bc5 100644 (file)
@@ -204,6 +204,11 @@ int main(int argc, char *argv[])
         return EXIT_FAILURE;
     }
 
+    if (pidfile_open(pidfile) < 0) {
+        crit("unable to write pidfile %s", pidfile);
+        return EXIT_FAILURE;
+    }
+
     if (drop_privileges(RUNAS_USER, RUNAS_GROUP) < 0) {
         crit("unable to drop privileges");
         return EXIT_FAILURE;
@@ -217,8 +222,14 @@ int main(int argc, char *argv[])
         config->port = port;
     }
 
-    if (common_setup(pidfile, true, NULL, NULL, daemonize) != EXIT_SUCCESS
-        || start_listener(config->port) < 0) {
+    if (daemonize && daemon_detach() < 0) {
+        crit("unable to fork");
+        return EXIT_FAILURE;
+    }
+
+    pidfile_refresh();
+
+    if (start_listener(config->port) < 0) {
         return EXIT_FAILURE;
     } else {
         return server_loop(query_starter, (delete_client_t)query_delete,
index 19fd0b2..c2704f1 100644 (file)
@@ -148,6 +148,86 @@ static trie_t *strlist_create(const char *file, bool reverse, bool lock)
     return db;
 }
 
+static bool strlist_create_from_rhbl(const char *file, bool lock,
+                                     trie_t **phosts, trie_t **pdomains)
+{
+    trie_t *hosts, *domains;
+    uint32_t host_count, domain_count;
+    file_map_t map;
+    const char *p, *end;
+    char line[BUFSIZ];
+
+    if (!file_map_open(&map, file, false)) {
+        return false;
+    }
+    p   = map.map;
+    end = map.end;
+    while (end > p && end[-1] != '\n') {
+        --end;
+    }
+    if (end != map.end) {
+        warn("file %s miss a final \\n, ignoring last line",
+             file);
+    }
+
+    hosts = trie_new();
+    host_count = 0;
+    domains = trie_new();
+    domain_count = 0;
+    while (p < end && p != NULL) {
+        const char *eol = (char *)memchr(p, '\n', end - p);
+        if (eol == NULL) {
+            eol = end;
+        }
+        if (eol - p >= BUFSIZ) {
+            err("unreasonnable long line");
+            file_map_close(&map);
+            trie_delete(&hosts);
+            trie_delete(&domains);
+            return false;
+        }
+        if (*p != '#') {
+            const char *eos = eol;
+            while (p < eos && isspace(*p)) {
+                ++p;
+            }
+            while (p < eos && isspace(eos[-1])) {
+                --eos;
+            }
+            if (p < eos) {
+                if (isalnum(*p)) {
+                    strlist_copy(line, p, eos - p, true);
+                    trie_insert(hosts, line);
+                    ++host_count;
+                } else if (*p == '*') {
+                    ++p;
+                    strlist_copy(line, p, eos - p, true);
+                    trie_insert(domains, line);
+                    ++domain_count;
+                }
+            }
+        }
+        p = eol + 1;
+    }
+    file_map_close(&map);
+    if (host_count > 0) {
+        trie_compile(hosts, lock);
+        *phosts = hosts;
+    } else {
+        trie_delete(&hosts);
+        *phosts = NULL;
+    }
+    if (domain_count > 0) {
+        trie_compile(domains, lock);
+        *pdomains = domains;
+    } else {
+        trie_delete(&domains);
+        *pdomains = NULL;
+    }
+    return hosts != NULL || domains != NULL;
+
+}
+
 
 static bool strlist_filter_constructor(filter_t *filter)
 {
@@ -165,7 +245,7 @@ static bool strlist_filter_constructor(filter_t *filter)
     foreach (filter_param_t *param, filter->params) {
         switch (param->type) {
           /* file parameter is:
-           *  [no]lock:(prefix|suffix):weight:filename
+           *  [no]lock:(partial-)(prefix|suffix):weight:filename
            *  valid options are:
            *    - lock:   memlock the database in memory.
            *    - nolock: don't memlock the database in memory.
@@ -241,6 +321,72 @@ static bool strlist_filter_constructor(filter_t *filter)
             }
           } break;
 
+          /* rbldns parameter is:
+           *  [no]lock::weight:filename
+           *  valid options are:
+           *    - lock:   memlock the database in memory.
+           *    - nolock: don't memlock the database in memory.
+           *    - \d+:    a number describing the weight to give to the match
+           *              the given list [mandatory]
+           *  directly import a file issued from a rhbl in rbldns format.
+           */
+          case ATK_RBLDNS: {
+            bool lock = false;
+            int  weight = 0;
+            trie_t *trie_hosts   = NULL;
+            trie_t *trie_domains = NULL;
+            const char *current = param->value;
+            const char *p = m_strchrnul(param->value, ':');
+            char *next = NULL;
+            for (int i = 0 ; i < 3 ; ++i) {
+                PARSE_CHECK(i == 2 || *p,
+                            "file parameter must contains a locking state "
+                            "and a weight option");
+                switch (i) {
+                  case 0:
+                    if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
+                        lock = true;
+                    } else if ((p - current) == 6 && strncmp(current, "nolock", 6) == 0) {
+                        lock = false;
+                    } else {
+                        PARSE_CHECK(false, "illegal locking state %.*s",
+                                    p - current, current);
+                    }
+                    break;
+
+                  case 1:
+                    weight = strtol(current, &next, 10);
+                    PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
+                                "illegal weight value %.*s",
+                                (p - current), current);
+                    break;
+
+                  case 2:
+                    PARSE_CHECK(strlist_create_from_rhbl(current, lock,
+                                                         &trie_hosts, &trie_domains),
+                                "cannot load string list from rhbl %s", current);
+                    if (trie_hosts != NULL) {
+                        array_add(config->tries, trie_hosts);
+                        array_add(config->weights, weight);
+                        array_add(config->reverses, true);
+                        array_add(config->partiales, false);
+                    }
+                    if (trie_domains != NULL) {
+                        array_add(config->tries, trie_domains);
+                        array_add(config->weights, weight);
+                        array_add(config->reverses, true);
+                        array_add(config->partiales, true);
+                    }
+                    config->is_hostname = true;
+                    break;
+                }
+                if (i != 2) {
+                    current = p + 1;
+                    p = m_strchrnul(current, ':');
+                }
+            }
+          } break;
+
           /* hard_threshold parameter is an integer.
            *  If the matching score is greater or equal than this threshold,
            *  the hook "hard_match" is called.
@@ -383,6 +529,7 @@ static int strlist_init(void)
     /* Parameters.
      */
     (void)filter_param_register(type, "file");
+    (void)filter_param_register(type, "rbldns");
     (void)filter_param_register(type, "hard_threshold");
     (void)filter_param_register(type, "soft_threshold");
     (void)filter_param_register(type, "fields");
index 057018f..9cafae1 100644 (file)
             __FILE__, __LINE__, __func__, ##__VA_ARGS__)
 
 #include "common.h"
-#include "iplist.c"
+#include "iplist.h"
+#include "array.h"
 
 int main(int argc, char *argv[])
 {
     if (argc > 1) {
         rbldb_t *db = rbldb_create(argv[1], false);
         printf("loaded: %s, %d ips, %d o\n", argv[1], rbldb_stats(db),
-               rbldb_stats(db) * 4);
+               rbldb_stats(db) * 1 + 65536 * sizeof(A(uint16_t)));
+
+        time_t now = time(NULL);
+        for (uint32_t i = 0 ; i < 1000000000 ; ++i) {
+            rbldb_ipv4_lookup(db, (88 << 24) | (170 << 16) | (239 << 8) | (132));
+        }
+        printf("%ld request per second\n", 1000000000 / (time(NULL) - now));
         rbldb_delete(&db);
     }
     return 0;
diff --git a/tools/postgrey2postlicyd b/tools/postgrey2postlicyd
new file mode 100755 (executable)
index 0000000..2c50b74
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+##############################################################################
+#          pfixtools: a collection of postfix related tools                  #
+#          ~~~~~~~~~                                                         #
+#  ________________________________________________________________________  #
+#                                                                            #
+#  Redistribution and use in source and binary forms, with or without        #
+#  modification, are permitted provided that the following conditions        #
+#  are met:                                                                  #
+#                                                                            #
+#  1. Redistributions of source code must retain the above copyright         #
+#     notice, this list of conditions and the following disclaimer.          #
+#  2. Redistributions in binary form must reproduce the above copyright      #
+#     notice, this list of conditions and the following disclaimer in the    #
+#     documentation and/or other materials provided with the distribution.   #
+#  3. The names of its contributors may not be used to endorse or promote    #
+#     products derived from this software without specific prior written     #
+#     permission.                                                            #
+#                                                                            #
+#  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND   #
+#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE     #
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR        #
+#  PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS    #
+#  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR    #
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF      #
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS  #
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN   #
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)   #
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF    #
+#  THE POSSIBILITY OF SUCH DAMAGE.                                           #
+##############################################################################
+
+# Copyright (c) 2008 Aymeric Augustin
+
+
+# Convert the postgrey_whitelist_clients file to a format
+# suitable for use with the postlicyd Postfix policy daemon
+
+
+import os, re, sys
+
+def process(infile, outfile):
+
+    # Write headers
+    file.write("# Do not edit, file autogenerated by %s\n" % sys.argv[0])
+    if len(sys.argv) > 1 and sys.argv[1] != '-':
+        file.write("# This file has been generated from %s\n" % sys.argv[1])
+
+    re_domain_name = re.compile(r'[a-z0-9.\-]+\.[a-z]+')
+    re_ip_address = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
+
+    # Store each entry to avoid duplicates
+    entries = []
+
+    for line in infile:
+        # Comments: keep them
+        if line == '\n' or line[0] == '#':
+            outfile.write(line)
+        # IP addresses: keep as is
+        elif re_ip_address.match(line):
+            outfile.write(line)
+        # Regexps: extract final constant part
+        elif line[0] == '/':
+            line = line.rstrip(r'$/').replace(r'\.', r'.')
+            host = re_domain_name.findall(line)[-1]
+            result = host + '\n'
+            if result not in entries:
+                entries.append(result)
+                outfile.write(result)
+        # Domain names: prepend a dot if the domain name contains only one dot
+        elif re_domain_name.match(line):
+            if line.count('.') < 2:
+                result = '.' + line
+            else:
+                result = line
+            if result not in entries:
+                entries.append(result)
+                outfile.write(result)
+        # Unrecognized: report on stderr and comment in output
+        else:
+            outfile.write('# IGNORED: ' + line)
+            sys.stderr.write("Couldn't process line: %s" % line)
+
+
+if __name__ == '__main__':
+
+    # Check number of arguments
+    if len(sys.argv) > 3:
+        print "Usage: %s [input] [output]" % sys.argv[0]
+        print "If input/output is omitted or -, stdin/stdout is used."
+        sys.exit(1)
+
+    # Parse first argument
+    if len(sys.argv) > 1 and sys.argv[1] != '-':
+        infile = open(sys.argv[1], 'r')
+    else:
+        infile = sys.stdin
+
+    # Parse second argument
+    if len(sys.argv) > 2 and sys.argv[2] != '-':
+        if sys.argv[1] == sys.argv[2]:
+            print "Source file and destination file are identical, aborting"
+            sys.exit(1)
+        if os.path.exists(sys.argv[2]):
+            print "Destination file %s already exists, aborting" % sys.argv[2]
+            sys.exit(1)
+        outfile = open(sys.argv[2], 'w')
+    else:
+        outfile = sys.stdout
+
+    # Do the processing
+    process(infile, outfile)
diff --git a/tools/rbldns2postlicyd b/tools/rbldns2postlicyd
new file mode 100755 (executable)
index 0000000..64966a0
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+##############################################################################
+#          pfixtools: a collection of postfix related tools                  #
+#          ~~~~~~~~~                                                         #
+#  ________________________________________________________________________  #
+#                                                                            #
+#  Redistribution and use in source and binary forms, with or without        #
+#  modification, are permitted provided that the following conditions        #
+#  are met:                                                                  #
+#                                                                            #
+#  1. Redistributions of source code must retain the above copyright         #
+#     notice, this list of conditions and the following disclaimer.          #
+#  2. Redistributions in binary form must reproduce the above copyright      #
+#     notice, this list of conditions and the following disclaimer in the    #
+#     documentation and/or other materials provided with the distribution.   #
+#  3. The names of its contributors may not be used to endorse or promote    #
+#     products derived from this software without specific prior written     #
+#     permission.                                                            #
+#                                                                            #
+#  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND   #
+#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE     #
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR        #
+#  PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS    #
+#  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR    #
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF      #
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS  #
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN   #
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)   #
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF    #
+#  THE POSSIBILITY OF SUCH DAMAGE.                                           #
+##############################################################################
+
+# Copyright (c) 2008 Florent Bruneau
+
+import os, re, sys
+
+def headers(file, source, type):
+    file.write("# Do not edit, file autogenerated by %s\n" % sys.argv[0])
+    file.write("# This file has been generated from %s blacklist\n" % source)
+    file.write("#   it contains %s matching rules for postlicyd\n" % type)
+
+def convert(rbl):
+    source  = open(rbl, "r")
+    hosts   = open("%s_hosts" % rbl, "w")
+    domains = open("%s_domains" % rbl, "w")
+
+    headers(hosts, rbl, "hosts")
+    headers(domains, rbl, "domains")
+
+    for line in source:
+        if line[0].isalnum():
+            hosts.write(line);
+        elif line[0] == '*':
+            domains.write(line[1:]);
+
+if __name__ == '__main__':
+    if len(sys.argv) == 1:
+        print "Usage: %s [input] ..." % sys.argv[0]
+        sys.exit(1)
+
+    map(convert, sys.argv[1:])
+
+# vim:set syntax=python: