Commits

Zhang Huangbin committed e85694f

Starting to merge iredapd.py and iredapd-rr.py.

Comments (0)

Files changed (44)

etc/iredapd-rr.ini.sample

-[general]
-# Listen address and port.
-listen_addr     = 127.0.0.1
-listen_port     = 7778
-
-# Run as a low privileged user.
-# If you don't want to create one, you can try 'nobody'.
-run_as_user     = iredapd
-
-# Background/daemon mode: yes, no.
-# Detach iredapd from terminal. It's recommended to always running as daemon.
-run_as_daemon   = yes
-
-# Path to pid file.
-pid_file        = /var/run/iredapd-rr.pid
-
-# Log type: file.
-# Note:
-#   - Currently, only 'file' type is supported.
-#   - If log_type is 'file', log_file is required.
-#   - If log_type is 'mysql', sql related info are required.
-log_type        = file
-log_file        = /var/log/iredapd-rr.log
-
-# Log level: info, error, debug.
-log_level       = error
-
-# Backend: ldap, mysql, pgsql.
-backend = ldap
-
-# Bypass clients listed in postfix 'mynetworks': yes, no.
-bypass_mynetworks = no
-
-[ldap]
-# For ldap backend only.
-# LDAP server setting.
-# Uri must starts with ldap:// or ldaps:// (TLS/SSL).
-#
-# Tip: You can get binddn, bindpw from /etc/postfix/ldap_*.cf.
-#
-uri         = ldap://127.0.0.1:389
-binddn      = cn=vmail,dc=iredmail,dc=org
-bindpw      = mRAEWpGRtlCs1O0QuWpXoaJ36EjRql
-basedn      = o=domains,dc=iredmail,dc=org
-
-# Enabled plugins.
-#   - Plugin name is file name which placed under 'src/plugins/' directory.
-#   - Plugin names MUST be seperated by comma.
-plugins = ldap_recipient_restrictions

etc/iredapd.ini.sample

-[general]
-# Listen address and port.
-listen_addr     = 127.0.0.1
-listen_port     = 7777
-
-# Run as a low privileged user.
-# If you don't want to create one, you can try 'nobody'.
-run_as_user     = iredapd
-
-# Background/daemon mode: yes, no.
-# Run iRedAPD as daemon, detach iredapd from terminal.
-run_as_daemon   = yes
-
-# Path to pid file.
-pid_file        = /var/run/iredapd.pid
-
-# Log type: file.
-# Set 'log_file = /dev/null' if you don't want to keep the log.
-log_type        = file
-log_file        = /var/log/iredapd.log
-
-# Log level: info, error, debug.
-log_level       = error
-
-# Backend: ldap, mysql, pgsql.
-backend = ldap
-
-[ldap]
-# For ldap backend only.
-# LDAP server setting.
-# Uri must starts with ldap:// or ldaps:// (TLS/SSL).
-#
-# Tip: You can get binddn, bindpw from /etc/postfix/ldap_*.cf.
-#
-uri         = ldap://127.0.0.1:389
-binddn      = cn=vmail,dc=iredmail,dc=org
-bindpw      = mRAEWpGRtlCs1O0QuWpXoaJ36EjRql
-basedn      = o=domains,dc=iredmail,dc=org
-
-# Enabled plugins.
-#   - Plugin name is file name which placed under 'src/plugins/' directory.
-#   - Plugin names MUST be seperated by comma.
-#
-# Available plugins:
-#   * ldap_domain_wblist: per-domain white/blacklist support.
-#       Note: If you want to enable this plugin, it's better to make it the
-#             first one in enabled plugin list.
-#   * ldap_maillist_access_policy: mail list deliver restrictions.
-#   * block_amavisd_blacklisted_senders: per-user white/blacklist support.
-plugins = ldap_maillist_access_policy, block_amavisd_blacklisted_senders
-
-[sql]
-# For MySQL and PostgreSQL backends
-server      = 127.0.0.1
-port        = 3306
-db          = vmail
-user        = vmail
-password    = Psaf68wsuVctYSbj4PJzRqmFsE0rlQ
-
-# Enabled plugins.
-#   - Plugin name is file name which placed under 'src/plugins/' directory,
-#     starts with 'sql_'.
-#   - Plugin names MUST be seperated by comma.
-plugins = sql_alias_access_policy
+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+import os
+import os.path
+import sys
+import pwd
+import ConfigParser
+import socket
+import asyncore
+import asynchat
+import logging
+
+# Append plugin directory.
+sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/plugins-rr')
+
+# Get config file.
+if len(sys.argv) != 2:
+    sys.exit('Usage: %s /path/to/iredapd-rr.ini')
+else:
+    config_file = sys.argv[1]
+
+    # Check file exists.
+    if not os.path.exists(config_file):
+        sys.exit('File not exist: %s.' % config_file)
+
+# Read configurations.
+cfg = ConfigParser.SafeConfigParser()
+cfg.read(config_file)
+
+from libs import __version__, SMTP_ACTIONS, daemon
+
+class apd_channel(asynchat.async_chat):
+    def __init__(self, conn, remote_addr):
+        asynchat.async_chat.__init__(self, conn)
+        self.remote_addr = remote_addr
+        self.buffer = []
+        self.smtp_attrs = {}
+        self.set_terminator('\n')
+        logging.debug("Connect from %s, port %s." % self.remote_addr)
+
+    def push(self, msg):
+        asynchat.async_chat.push(self, msg + '\n')
+
+    def collect_incoming_data(self, data):
+        self.buffer.append(data)
+
+    def found_terminator(self):
+        if len(self.buffer) is not 0:
+            line = self.buffer.pop()
+            logging.debug("smtp session: " + line)
+            if line.find('=') != -1:
+                key = line.split('=')[0]
+                value = line.split('=', 1)[1]
+                self.smtp_attrs[key] = value
+        elif len(self.smtp_attrs) != 0:
+            try:
+                if cfg.get('general', 'backend', 'ldap') == 'ldap':
+                    modeler = LDAPModeler()
+                else:
+                    modeler = SQLModeler()
+
+                result = modeler.handle_data(self.smtp_attrs)
+                if result:
+                    action = result
+                else:
+                    action = SMTP_ACTIONS['default']
+                logging.debug("Final action: %s." % str(result))
+            except Exception, e:
+                action = SMTP_ACTIONS['default']
+                logging.debug('Error: %s. Use default action instead: %s' %
+                        (str(e), str(action)))
+
+            # Log final action.
+            logging.info('[%s] %s -> %s, %s' % (self.smtp_attrs['client_address'],
+                                                self.smtp_attrs['sender'],
+                                                self.smtp_attrs['recipient'],
+                                                action,
+                                               ))
+
+            self.push('action=' + action + '\n')
+            asynchat.async_chat.handle_close(self)
+            logging.debug("Connection closed")
+        else:
+            action = SMTP_ACTIONS['defer']
+            logging.debug("replying: " + action)
+            self.push(action + '\n')
+            asynchat.async_chat.handle_close(self)
+            logging.debug("Connection closed")
+
+
+class apd_socket(asyncore.dispatcher):
+    def __init__(self, localaddr):
+        asyncore.dispatcher.__init__(self)
+        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.set_reuse_addr()
+        self.bind(localaddr)
+        self.listen(5)
+        ip, port = localaddr
+        logging.info("Starting iredapd (v%s, pid: %d), listening on %s:%s." %
+                (__version__, os.getpid(), ip, str(port)))
+
+    def handle_accept(self):
+        conn, remote_addr = self.accept()
+        channel = apd_channel(conn, remote_addr)
+
+
+class SQLModeler:
+    def __init__(self):
+        import web
+
+        # Turn off debug mode.
+        web.config.debug = False
+
+        self.db = web.database(
+            dbn='mysql',
+            host=cfg.get('sql', 'server', 'localhost'),
+            db=cfg.get('sql', 'db', 'vmail'),
+            user=cfg.get('sql', 'user', 'vmail'),
+            pw=cfg.get('sql', 'password'),
+        )
+
+    def handle_data(self, map):
+        if 'sender' in map.keys() and 'recipient' in map.keys():
+            # Get plugin module name and convert plugin list to python list type.
+            self.plugins = cfg.get('sql', 'plugins', '')
+            self.plugins = [v.strip() for v in self.plugins.split(',')]
+
+            # Get sender, recipient.
+            # Sender/recipient are used almost in all plugins, so store them
+            # a dict and pass to plugins.
+            senderReceiver = {
+                'sender': map['sender'],
+                'recipient': map['recipient'],
+                'sender_domain': map['sender'].split('@')[-1],
+                'recipient_domain': map['recipient'].split('@')[-1],
+            }
+
+            if len(self.plugins) > 0:
+                #
+                # Import plugin modules.
+                #
+                self.modules = []
+
+                # Load plugin module.
+                for plugin in self.plugins:
+                    try:
+                        self.modules.append(__import__(plugin))
+                    except ImportError:
+                        # Print error message if plugin module doesn't exist.
+                        # Use logging.info to let admin know this critical error.
+                        logging.info('Error: plugin %s.py not exist.' % plugin)
+                    except Exception, e:
+                        logging.debug('Error while importing plugin module (%s): %s' % (plugin, str(e)))
+
+                #
+                # Apply plugins.
+                #
+                for module in self.modules:
+                    try:
+                        logging.debug('Apply plugin (%s).' % (module.__name__, ))
+                        pluginAction = module.restriction(
+                            dbConn=self.db,
+                            senderReceiver=senderReceiver,
+                            smtpSessionData=map,
+                        )
+
+                        logging.debug('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                        if not pluginAction.startswith('DUNNO'):
+                            logging.info('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                            return pluginAction
+
+                        return 'DUNNO'
+                    except Exception, e:
+                        logging.debug('Error while apply plugin (%s): %s' % (module, str(e)))
+
+            else:
+                # No plugins available.
+                return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['defer']
+
+
+
+class LDAPModeler:
+    def __init__(self):
+        import ldap
+
+        self.ldap = ldap
+
+        # Read LDAP server settings.
+        self.uri = cfg.get('ldap', 'uri', 'ldap://127.0.0.1:389')
+        self.binddn = cfg.get('ldap', 'binddn')
+        self.bindpw = cfg.get('ldap', 'bindpw')
+        self.baseDN = cfg.get('ldap', 'basedn')
+
+        # Initialize ldap connection.
+        try:
+            self.conn = self.ldap.initialize(self.uri)
+            logging.debug('LDAP connection initialied success.')
+        except Exception, e:
+            logging.error('LDAP initialized failed: %s.' % str(e))
+            sys.exit()
+
+        # Bind to ldap server.
+        if self.binddn != '' and self.bindpw != '':
+            try:
+                self.conn.bind_s(self.binddn, self.bindpw)
+                logging.debug('LDAP bind success.')
+            except self.ldap.INVALID_CREDENTIALS:
+                logging.error('LDAP bind failed: incorrect bind dn or password.')
+                sys.exit()
+            except Exception, e:
+                logging.error('LDAP bind failed: %s.' % str(e))
+                sys.exit()
+
+    def __get_sender_dn_ldif(self, sender):
+        logging.debug('__get_sender_dn_ldif (sender): %s' % sender)
+
+        if len(sender) < 6 or sender is None:
+            logging.debug('__get_sender_dn_ldif: Sender is not a valid email address.')
+            return (None, None)
+
+        try:
+            logging.debug('__get_sender_dn_ldif: Quering LDAP')
+            result = self.conn.search_s(
+                    self.baseDN,
+                    self.ldap.SCOPE_SUBTREE,
+                    '(&(|(mail=%s)(shadowAddress=%s))(|(objectClass=mailUser)(objectClass=mailList)(objectClass=mailAlias)))' % (sender, sender),
+                    )
+            logging.debug('__get_sender_dn_ldif (result): %s' % str(result))
+            if len(result) == 0:
+                return (None, None)
+            else:
+                return (result[0][0], result[0][1])
+        except Exception, e:
+            logging.debug('!!! ERROR !!! __get_sender_dn_ldif (result): %s' % str(e))
+            return (None, None)
+
+    def handle_data(self, map):
+        if 'sender' in map.keys() and 'recipient' in map.keys():
+            # Get plugin module name and convert plugin list to python list type.
+            self.plugins = cfg.get('ldap', 'plugins', '')
+            self.plugins = [v.strip() for v in self.plugins.split(',')]
+
+            if len(self.plugins) > 0:
+
+                # Get account dn and LDIF data.
+                bypass_mynetworks = cfg.get('general', 'bypass_mynetworks', 'no')
+                if bypass_mynetworks == 'yes':
+                    sender = map['sasl_username']
+                else:
+                    sender = map['sender']
+
+                senderDN, senderLdif = self.__get_sender_dn_ldif(sender)
+
+                # Return if recipient account doesn't exist.
+                if senderDN is None or senderLdif is None:
+                    logging.debug('Sender DN or LDIF is none.')
+                    return SMTP_ACTIONS['default']
+
+                #
+                # Import plugin modules.
+                #
+                self.modules = []
+
+                # Load plugin module.
+                for plugin in self.plugins:
+                    try:
+                        self.modules.append(__import__(plugin))
+                    except ImportError:
+                        # Print error message if plugin module doesn't exist.
+                        # Use logging.info to let admin know this critical error.
+                        logging.info('Error: plugin %s.py not exist.' % plugin)
+                    except Exception, e:
+                        logging.debug('Error while importing plugin module (%s): %s' % (plugin, str(e)))
+
+                #
+                # Apply plugins.
+                #
+                for module in self.modules:
+                    try:
+                        logging.debug('Apply plugin (%s).' % (module.__name__, ))
+                        pluginAction = module.restriction(
+                            ldapConn=self.conn,
+                            ldapBaseDn=self.baseDN,
+                            ldapSenderDn=senderDN,
+                            ldapSenderLdif=senderLdif,
+                            smtpSessionData=map,
+                        )
+
+                        logging.debug('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                        if not pluginAction.startswith('DUNNO'):
+                            logging.info('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                            return pluginAction
+
+                        return 'DUNNO'
+                    except Exception, e:
+                        logging.debug('Error while apply plugin (%s): %s' % (module, str(e)))
+
+            else:
+                # No plugins available.
+                return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['defer']
+
+
+def main():
+    # Set umask.
+    os.umask(0077)
+
+    # Get listen address/port.
+    listen_addr = cfg.get('general', 'listen_addr', '127.0.0.1')
+    listen_port = int(cfg.get('general', 'listen_port', '7777'))
+
+    run_as_daemon = cfg.get('general', 'run_as_daemon', 'yes')
+
+    # Get log level.
+    log_level = getattr(logging, cfg.get('general', 'log_level', 'info').upper())
+
+    # Initialize file based logger.
+    if cfg.get('general', 'log_type', 'file') == 'file':
+        if run_as_daemon == 'yes':
+            logging.basicConfig(
+                    level=log_level,
+                    format='%(asctime)s %(levelname)s %(message)s',
+                    datefmt='%Y-%m-%d %H:%M:%S',
+                    filename=cfg.get('general', 'log_file', '/var/log/iredapd.log'),
+                    )
+        else:
+            logging.basicConfig(
+                    level=log_level,
+                    format='%(asctime)s %(levelname)s %(message)s',
+                    datefmt='%Y-%m-%d %H:%M:%S',
+                    )
+
+    # Initialize policy daemon.
+    socket_daemon = apd_socket((listen_addr, listen_port))
+
+    # Run this program as daemon.
+    if run_as_daemon == 'yes':
+        daemon.daemonize()
+
+    # Run as a low privileged user.
+    run_as_user = cfg.get('general', 'run_as_user', 'nobody')
+    uid = pwd.getpwnam(run_as_user)[2]
+
+    try:
+        # Write pid number into pid file.
+        f = open(cfg.get('general', 'pid_file', '/var/run/iredapd.pid'), 'w')
+        f.write(str(os.getpid()))
+        f.close()
+
+        # Set uid.
+        os.setuid(uid)
+
+        # Starting loop.
+        asyncore.loop()
+    except KeyboardInterrupt:
+        pass
+
+if __name__ == '__main__':
+    main()

iredapd.ini.sample

+[general]
+# Listen address and port.
+listen_addr     = 127.0.0.1
+listen_port     = 7777
+
+# Run as a low privileged user.
+# If you don't want to create one, you can try 'nobody'.
+run_as_user     = iredapd
+
+# Background/daemon mode: yes, no.
+# Run iRedAPD as daemon, detach iredapd from terminal.
+run_as_daemon   = yes
+
+# Path to pid file.
+pid_file        = /var/run/iredapd.pid
+
+# Log type: file.
+# Set 'log_file = /dev/null' if you don't want to keep the log.
+log_type        = file
+log_file        = /var/log/iredapd.log
+
+# Log level: info, error, debug.
+log_level       = error
+
+# Backend: ldap, mysql, pgsql.
+backend = ldap
+
+[ldap]
+# For ldap backend only.
+# LDAP server setting.
+# Uri must starts with ldap:// or ldaps:// (TLS/SSL).
+#
+# Tip: You can get binddn, bindpw from /etc/postfix/ldap_*.cf.
+#
+uri         = ldap://127.0.0.1:389
+binddn      = cn=vmail,dc=iredmail,dc=org
+bindpw      = mRAEWpGRtlCs1O0QuWpXoaJ36EjRql
+basedn      = o=domains,dc=iredmail,dc=org
+
+# Enabled plugins.
+#   - Plugin name is file name which placed under 'src/plugins/' directory.
+#   - Plugin names MUST be seperated by comma.
+#
+# Available plugins:
+#   * ldap_domain_wblist: per-domain white/blacklist support.
+#       Note: If you want to enable this plugin, it's better to make it the
+#             first one in enabled plugin list.
+#   * ldap_maillist_access_policy: mail list deliver restrictions.
+#   * block_amavisd_blacklisted_senders: per-user white/blacklist support.
+plugins = ldap_maillist_access_policy, block_amavisd_blacklisted_senders
+
+[sql]
+# For MySQL and PostgreSQL backends
+server      = 127.0.0.1
+port        = 3306
+db          = vmail
+user        = vmail
+password    = Psaf68wsuVctYSbj4PJzRqmFsE0rlQ
+
+# Enabled plugins.
+#   - Plugin name is file name which placed under 'src/plugins/' directory,
+#     starts with 'sql_'.
+#   - Plugin names MUST be seperated by comma.
+plugins = sql_alias_access_policy
+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+import os
+import os.path
+import sys
+import pwd
+import ConfigParser
+import socket
+import asyncore
+import asynchat
+import logging
+
+# Append plugin directory.
+sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/plugins')
+
+# Get config file.
+if len(sys.argv) != 2:
+    sys.exit('Usage: %s /path/to/iredapd.ini')
+else:
+    config_file = sys.argv[1]
+
+    # Check file exists.
+    if not os.path.exists(config_file):
+        sys.exit('File not exist: %s.' % config_file)
+
+# Read configurations.
+cfg = ConfigParser.SafeConfigParser()
+cfg.read(config_file)
+backend = cfg.get('general', 'backend', 'ldap')
+
+if backend == 'ldap':
+    from libs.ldaplib import LDAPModeler as Modeler
+    plugins = cfg.get('ldap', 'plugins', '')
+elif backend in ['mysql', 'pgsql']:
+    from libs.sqllib import SQLModeler as Modeler
+    plugins = cfg.get('sql', 'plugins', '')
+else:
+    sys.exit('Invalid backend, it must be ldap, mysql or pgsql.')
+
+from libs import __version__, SMTP_ACTIONS, daemon
+
+
+class apd_channel(asynchat.async_chat):
+    def __init__(self, conn, remote_addr):
+        asynchat.async_chat.__init__(self, conn)
+        self.remote_addr = remote_addr
+        self.buffer = []
+        self.smtp_attrs = {}
+        self.set_terminator('\n')
+        logging.debug("Connect from %s, port %s." % self.remote_addr)
+
+    def push(self, msg):
+        asynchat.async_chat.push(self, msg + '\n')
+
+    def collect_incoming_data(self, data):
+        self.buffer.append(data)
+
+    def found_terminator(self):
+        if self.buffer:
+            line = self.buffer.pop()
+            logging.debug("smtp session: " + line)
+            if line.find('=') != -1:
+                key = line.split('=')[0]
+                value = line.split('=', 1)[1]
+                self.smtp_attrs[key] = value
+        elif len(self.smtp_attrs) != 0:
+            try:
+                modeler = Modeler(cfg=cfg, logger=logging)
+
+                result = modeler.handle_data(self.smtp_attrs)
+                if result:
+                    action = result
+                else:
+                    action = SMTP_ACTIONS['default']
+                logging.debug("Final action: %s." % str(result))
+            except Exception, e:
+                action = SMTP_ACTIONS['default']
+                logging.debug('Error: %s. Use default action instead: %s' %
+                        (str(e), str(action)))
+
+            # Log final action.
+            logging.info('[%s] %s -> %s, %s' % (self.smtp_attrs['client_address'],
+                                                self.smtp_attrs['sender'],
+                                                self.smtp_attrs['recipient'],
+                                                action,
+                                               ))
+
+            self.push('action=' + action + '\n')
+            asynchat.async_chat.handle_close(self)
+            logging.debug("Connection closed")
+        else:
+            action = SMTP_ACTIONS['defer']
+            logging.debug("replying: " + action)
+            self.push(action + '\n')
+            asynchat.async_chat.handle_close(self)
+            logging.debug("Connection closed")
+
+
+class apd_socket(asyncore.dispatcher):
+    def __init__(self, localaddr):
+        asyncore.dispatcher.__init__(self)
+        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.set_reuse_addr()
+        self.bind(localaddr)
+        self.listen(5)
+        ip, port = localaddr
+        logging.info("Starting iRedAPD (version %s, %s backend), listening on %s:%d." % (__version__, backend, ip, port))
+        logging.info("Enabled plugin(s): %s." % (plugins))
+
+    def handle_accept(self):
+        conn, remote_addr = self.accept()
+        channel = apd_channel(conn, remote_addr)
+
+
+def main():
+    # Set umask.
+    os.umask(0077)
+
+    # Get listen address/port.
+    listen_addr = cfg.get('general', 'listen_addr', '127.0.0.1')
+    listen_port = int(cfg.get('general', 'listen_port', '7777'))
+
+    run_as_daemon = cfg.get('general', 'run_as_daemon', 'yes')
+
+    # Get log level.
+    log_level = getattr(logging, cfg.get('general', 'log_level', 'info').upper())
+
+    # Initialize file based logger.
+    if cfg.get('general', 'log_type', 'file') == 'file':
+        if run_as_daemon == 'yes':
+            logging.basicConfig(
+                    level=log_level,
+                    format='%(asctime)s %(levelname)s %(message)s',
+                    datefmt='%Y-%m-%d %H:%M:%S',
+                    filename=cfg.get('general', 'log_file', '/var/log/iredapd.log'),
+                    )
+        else:
+            logging.basicConfig(
+                    level=log_level,
+                    format='%(asctime)s %(levelname)s %(message)s',
+                    datefmt='%Y-%m-%d %H:%M:%S',
+                    )
+
+    # Initialize policy daemon.
+    socket_daemon = apd_socket((listen_addr, listen_port))
+
+    # Run this program as daemon.
+    if run_as_daemon == 'yes':
+        daemon.daemonize()
+
+    # Run as a low privileged user.
+    run_as_user = cfg.get('general', 'run_as_user', 'nobody')
+    uid = pwd.getpwnam(run_as_user)[2]
+
+    try:
+        # Write pid number into pid file.
+        f = open(cfg.get('general', 'pid_file', '/var/run/iredapd.pid'), 'w')
+        f.write(str(os.getpid()))
+        f.close()
+
+        # Set uid.
+        os.setuid(uid)
+
+        # Starting loop.
+        asyncore.loop()
+    except KeyboardInterrupt:
+        pass
+
+if __name__ == '__main__':
+    main()
+__author__ = 'Zhang Huangbin <zhb@iredmail.org>'
+__version__ = '1.3.9'
+
+SMTP_ACTIONS = {'accept': 'OK',
+                'defer': 'DEFER_IF_PERMIT Service temporarily unavailable',
+                'reject': 'REJECT Not authorized',
+                'default': 'DUNNO',
+               }
+#!/usr/bin/env python
+#
+# $Id: daemon.py 7274 2008-03-05 01:00:09Z bmc $
+
+# NOTE: Documentation is intended to be processed by epydoc and contains
+# epydoc markup.
+
+"""
+Overview
+========
+
+Convert the calling process to a daemon. To make the current Python process
+into a daemon process, you need two lines of code::
+
+    import daemon
+    daemon.daemonize()
+
+If C{daemonize()} fails for any reason, it throws an exception. It also
+logs debug messages, using the standard Python 'logging' package, to
+channel 'daemon'.
+
+Adapted from:
+
+  - U{http://www.clapper.org/software/daemonize/}
+
+See Also
+========
+
+Stevens, W. Richard. I{Unix Network Programming} (Addison-Wesley, 1990).
+"""
+
+__version__   = "1.0.1"
+__author__    = "Brian Clapper, bmc@clapper.org"
+__url__       = "http://www.clapper.org/software/python/daemon/"
+__copyright__ = "(c) 2008 Brian M. Clapper"
+__license__   = "BSD-style license"
+
+__all__ = ['daemonize', 'DaemonError']
+
+# ---------------------------------------------------------------------------
+# Imports
+# ---------------------------------------------------------------------------
+
+import logging
+import os
+import sys
+import signal
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+# Default daemon parameters.
+# File mode creation mask of the daemon.
+UMASK = 0077
+
+# Default working directory for the daemon.
+WORKDIR = "/"
+
+# Default maximum for the number of available file descriptors.
+MAXFD = 1024
+
+# The standard I/O file descriptors are redirected to /dev/null by default.
+if (hasattr(os, "devnull")):
+    NULL_DEVICE = os.devnull
+else:
+    NULL_DEVICE = "/dev/null"
+
+
+# ---------------------------------------------------------------------------
+# Logging
+# ---------------------------------------------------------------------------
+
+log = logging.getLogger('daemonize')
+
+# ---------------------------------------------------------------------------
+# Public classes
+# ---------------------------------------------------------------------------
+
+class DaemonError(Exception):
+    """
+    Thrown by C{daemonize()} when an error occurs while attempting to create
+    a daemon. A C{DaemonException} object always contains a single string
+    value that contains an error message describing the problem.
+    """
+    def __init__(self, errorMessage):
+        """
+        Create a new C{DaemonException}.
+
+        @type errorMessage:  string
+        @param errorMessage: the error message
+        """
+        self.errorMessage = errorMessage
+
+    def __str__(self):
+        """
+        Get a string version of the exception.
+
+        @return: a string representing the exception
+        """
+        return self.errorMessage
+
+# ---------------------------------------------------------------------------
+# Public functions
+# ---------------------------------------------------------------------------
+
+def daemonize(noClose=False):
+    """
+    Convert the calling process into a daemon.
+
+    @type noClose:  boolean
+    @param noClose: If True, don't close the file descriptors. Useful
+                    if the calling process has already redirected file
+                    descriptors to an output file. WARNING: Only set this
+                    parameter to True if you're SURE there are no open file
+                    descriptors to the calling terminal. Otherwise, you'll
+                    risk having the daemon re-acquire a control terminal,
+                    which can cause it to be killed if someone logs off that
+                    terminal.
+
+    @raise DaemonException: Error during daemonizing
+    """
+    global log
+
+    if os.name != 'posix':
+        log.warn('Daemon is only supported on Posix-compliant systems.')
+        return
+
+    try:
+        # Fork once to go into the background.
+
+        log.debug('Forking first child.')
+        pid = _fork()
+        if pid != 0:
+            # Parent. Exit using os._exit(), which doesn't fire any atexit
+            # functions.
+            os._exit(0)
+    
+        # First child. Create a new session. os.setsid() creates the session
+        # and makes this (child) process the process group leader. The process
+        # is guaranteed not to have a control terminal.
+        log.debug('Creating new session')
+        os.setsid()
+    
+        # Ignore SIGHUP
+        signal.signal(signal.SIGHUP, signal.SIG_IGN)
+
+        # Fork a second child to ensure that the daemon never reacquires
+        # a control terminal.
+        log.debug('Forking second child.')
+        pid = _fork()
+        if pid != 0:
+            # Original child. Exit.
+            os._exit(0)
+            
+        # This is the second child. Set the umask.
+        log.debug('Setting umask')
+        os.umask(UMASK)
+    
+        # Go to a neutral corner (i.e., the primary file system, so
+        # the daemon doesn't prevent some other file system from being
+        # unmounted).
+        log.debug('Changing working directory to "%s"' % WORKDIR)
+        os.chdir(WORKDIR)
+    
+        # Unless noClose was specified, close all file descriptors.
+        if not noClose:
+            log.debug('Redirecting file descriptors')
+            _redirectFileDescriptors()
+
+    except DaemonException:
+        raise
+
+    except OSError, e:
+        raise DaemonException('Error during daemonizing: %s [%d]' %\
+              (e.strerror, e.errno))
+            
+
+# ---------------------------------------------------------------------------
+# Private functions
+# ---------------------------------------------------------------------------
+
+def _fork():
+    try:
+        return os.fork()
+    except OSError, e:
+        raise DaemonException, 'Cannot fork: %s [%d]' % (e.strerror, e.errno)
+
+def _redirectFileDescriptors():
+    import resource  # POSIX resource information
+    maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+    if maxfd == resource.RLIM_INFINITY:
+        maxfd = MAXFD
+
+    # Close all file descriptors.
+
+    for fd in range(0, maxfd):
+        # Only close TTYs.
+        try:
+            os.ttyname(fd)
+        except:
+            continue
+
+        try:
+            os.close(fd)
+        except OSError:
+            # File descriptor wasn't open. Ignore.
+            pass
+
+    # Redirect standard input, output and error to something safe.
+    # os.open() is guaranteed to return the lowest available file
+    # descriptor (0, or standard input). Then, we can dup that descriptor
+    # for standard output and standard error.
+
+    os.open(NULL_DEVICE, os.O_RDWR)
+    os.dup2(0, 1)
+    os.dup2(0, 2)
+
+# ---------------------------------------------------------------------------
+# Main program (for testing)
+# ---------------------------------------------------------------------------
+
+if __name__ == '__main__':
+
+    log = logging.getLogger('daemon')
+    hdlr = logging.StreamHandler(sys.stdout)
+    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%T')
+    hdlr.setFormatter(formatter)
+    log.addHandler(hdlr) 
+    log.setLevel(logging.DEBUG)
+
+    log.debug('Before daemonizing, PID=%d' % os.getpid())
+    daemonize(noClose=True)
+    log.debug('After daemonizing, PID=%d' % os.getpid())
+    log.debug('Daemon is sleeping for 10 seconds')
+
+    import time
+    time.sleep(10)
+
+    log.debug('Daemon exiting')
+    sys.exit(0)
+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+import sys
+import ldap
+from libs import SMTP_ACTIONS
+
+
+class LDAPModeler:
+    def __init__(self, cfg, logger):
+        self.cfg = cfg
+        self.logger = logger
+
+        # Read LDAP server related settings.
+        self.uri = self.cfg.get('ldap', 'uri', 'ldap://127.0.0.1:389')
+        self.binddn = self.cfg.get('ldap', 'binddn')
+        self.bindpw = self.cfg.get('ldap', 'bindpw')
+        self.baseDN = self.cfg.get('ldap', 'basedn')
+
+        # Initialize ldap connection.
+        try:
+            self.conn = ldap.initialize(self.uri)
+            self.logger.debug('LDAP connection initialied success.')
+        except Exception, e:
+            self.logger.error('LDAP initialized failed: %s.' % str(e))
+            sys.exit()
+
+        # Bind to ldap server.
+        try:
+            self.conn.bind_s(self.binddn, self.bindpw)
+            self.logger.debug('LDAP bind success.')
+        except ldap.INVALID_CREDENTIALS:
+            self.logger.error('LDAP bind failed: incorrect bind dn or password.')
+            sys.exit()
+        except Exception, e:
+            self.logger.error('LDAP bind failed: %s.' % str(e))
+            sys.exit()
+
+    def __del__(self):
+        try:
+            self.conn.unbind_s()
+            self.logger.debug('Close LDAP connection.')
+        except Exception, e:
+            self.logger.debug('Error while closing connection: %s' % str(e))
+
+    def __get_recipient_dn_ldif(self, recipient):
+        self.logger.debug('__get_recipient_dn_ldif (recipient): %s' % recipient)
+        try:
+            filter = '(&(|(mail=%s)(shadowAddress=%s))(|(objectClass=mailUser)(objectClass=mailList)(objectClass=mailAlias)))' % (recipient, recipient)
+            self.logger.debug('__get_recipient_dn_ldif (ldap query filter): %s' % filter)
+
+            result = self.conn.search_s(self.baseDN, ldap.SCOPE_SUBTREE, filter)
+
+            if len(result) == 1:
+                self.logger.debug('__get_recipient_dn_ldif (ldap query result): %s' % str(result))
+                dn, entry = result[0]
+                return (dn, entry)
+            else:
+                self.logger.debug('__get_recipient_dn_ldif: Can not find recipient in LDAP server.')
+                return (None, None)
+        except Exception, e:
+            self.logger.debug('!!! ERROR !!! __get_recipient_dn_ldif (result): %s' % str(e))
+            return (None, None)
+
+    def __get_access_policy(self, recipient):
+        """Get access policy of mail list.
+
+        return (dn_of_mail_list, value_of_access_policy,)"""
+
+        self.logger.debug('__get_access_policy (list): %s' % recipient)
+
+        # Replace 'recipient' placehold in config file with mail list address.
+        try:
+            self.cfg.set('ldap', "recipient", recipient)
+        except Exception, e:
+            self.logger.error("""Error while replacing 'recipient': %s""" % (str(e)))
+
+        # Search mail list object.
+        searchBasedn = 'mail=%s,ou=Groups,domainName=%s,%s' % (recipient, recipient.split('@')[1], self.baseDN)
+        searchScope = ldap.SCOPE_BASE
+        searchFilter = self.cfg.get('ldap', 'filter_maillist')
+        searchAttr = self.cfg.get('ldap', 'attr_access_policy', 'accessPolicy')
+
+        self.logger.debug('__get_access_policy (searchBasedn): %s' % searchBasedn)
+        self.logger.debug('__get_access_policy (searchScope): %s' % searchScope)
+        self.logger.debug('__get_access_policy (searchFilter): %s' % searchFilter)
+        self.logger.debug('__get_access_policy (searchAttr): %s' % searchAttr)
+
+        try:
+            result = self.conn.search_s(searchBasedn, searchScope, searchFilter, [searchAttr])
+            self.logger.debug('__get_access_policy (search result): %s' % str(result))
+        except ldap.NO_SUCH_OBJECT:
+            self.logger.debug('__get_access_policy (not a mail list: %s) Returned (None)' % recipient)
+            return (None, None)
+        except Exception, e:
+            self.logger.debug('__get_access_policy (ERROR while searching list): %s' % str(e))
+            return (None, None)
+
+        if len(result) != 1:
+            return (None, None)
+        else:
+            # Example of result data:
+            # [('dn', {'accessPolicy': ['value']})]
+            listdn = result[0][0]
+            listpolicy = result[0][1][searchAttr][0]
+            returnVal = (listdn, listpolicy)
+
+            self.logger.debug('__get_access_policy (returned): %s' % str(returnVal))
+            return returnVal
+
+    def __get_allowed_senders(self, listdn, recipient, listpolicy, sender=''):
+        """return search_result_list_based_on_access_policy"""
+        self.logger.debug('__get_allowed_senders (listpolicy): %s' % listpolicy)
+
+        # Replace 'recipient' and 'sender' with email addresses.
+        self.cfg.set("ldap", "recipient", recipient)
+        self.cfg.set("ldap", "sender", sender)
+
+        # Set search base dn, scope, filter and attribute list based on access policy.
+        if listpolicy == 'membersOnly':
+            baseDN = self.baseDN
+            searchScope = ldap.SCOPE_SUBTREE
+            # Filter used to get mail list members.
+            searchFilter = self.cfg.get("ldap", "filter_member")
+            searchAttr = self.cfg.get("ldap", "attr_member")
+        else:
+            baseDN = listdn
+            # Use SCOPE_BASE to improve performance.
+            searchScope = ldap.SCOPE_BASE
+            # Filter used to get mail list moderators.
+            searchFilter = self.cfg.get("ldap", "filter_allowed_senders")
+            searchAttr = self.cfg.get("ldap", "attr_moderator")
+
+        self.logger.debug('__get_allowed_senders (baseDN): %s' % baseDN)
+        self.logger.debug('__get_allowed_senders (searchScope): %s' % searchScope)
+        self.logger.debug('__get_allowed_senders (searchFilter): %s' % searchFilter)
+        self.logger.debug('__get_allowed_senders (searchAttr): %s' % searchAttr)
+
+        try:
+            result = self.conn.search_s(baseDN, searchScope, searchFilter, [searchAttr])
+            self.logger.debug('__get_allowed_senders (search result): %s' % str(result))
+        except ldap.NO_SUCH_OBJECT:
+            self.logger.debug('__get_allowed_senders (not a mail list: %s) Returned (None)' % recipient)
+            return None
+        except Exception, e:
+            self.logger.debug('__get_allowed_senders (ERROR while searching list): %s' % str(e))
+            return None
+
+        if len(result) != 1:
+            return None
+        else:
+            # Example of result data:
+            # [('dn', {'listAllowedUser': ['user@domain.ltd']})]
+            return result[0][1][searchAttr]
+
+    def __get_smtp_action(self, recipient, sender):
+        """return smtp_action"""
+        listdn, listpolicy = self.__get_access_policy(recipient)
+
+        if listdn is None or listpolicy is None:
+            return None
+        else:
+            if listpolicy == "public":
+                # No restriction.
+                return SMTP_ACTIONS['accept']
+            elif listpolicy == "domain":
+                # Allow all users under the same domain.
+                if sender.split('@')[1] == recipient.split('@')[1]:
+                    return SMTP_ACTIONS['accept']
+                else:
+                    return SMTP_ACTIONS['reject']
+            elif listpolicy == "allowedOnly":
+                # Bypass allowed users only.
+                allowed_senders = self.__get_allowed_senders(listdn, recipient, 'allowedOnly', sender)
+
+                if allowed_senders is not None:
+                    addresses = set(allowed_senders)    # Remove duplicate addresses.
+                    if sender in addresses:
+                        return SMTP_ACTIONS['accept']
+                    else:
+                        return SMTP_ACTIONS['reject']
+                else:
+                    return SMTP_ACTIONS['reject']
+            elif listpolicy == "membersOnly":
+                allowed_senders = self.__get_allowed_senders(listdn, recipient, 'membersOnly', sender)
+
+                if allowed_senders is not None:
+                    addresses = set(allowed_senders)
+                    if sender in addresses:
+                        return SMTP_ACTIONS['accept']
+                    else:
+                        return SMTP_ACTIONS['reject']
+                else:
+                    return SMTP_ACTIONS['reject']
+
+    def handle_data(self, map):
+        if 'sender' in map.keys() and 'recipient' in map.keys():
+            if len(map['sender']) < 6:
+                # Not a valid email address.
+                return 'DUNNO'
+
+            # Get plugin module name and convert plugin list to python list type.
+            self.plugins = self.cfg.get('ldap', 'plugins', '')
+            self.plugins = [v.strip() for v in self.plugins.split(',')]
+
+            if self.plugins:
+
+                # Get account dn and LDIF data.
+                recipientDn, recipientLdif = self.__get_recipient_dn_ldif(map['recipient'])
+
+                # Return if recipient account doesn't exist.
+                if recipientDn is None or recipientLdif is None:
+                    self.logger.debug('Recipient DN or LDIF is None.')
+                    return SMTP_ACTIONS['default']
+
+                #
+                # Import plugin modules.
+                #
+                self.modules = []
+
+                # Load plugin module.
+                for plugin in self.plugins:
+                    try:
+                        self.modules.append(__import__(plugin))
+                    except ImportError:
+                        # Print error message if plugin module doesn't exist.
+                        # Use self.logger.info to let admin know this critical error.
+                        self.logger.info('Error: plugin %s.py not exist.' % plugin)
+                    except Exception, e:
+                        self.logger.debug('Error while importing plugin module (%s): %s' % (plugin, str(e)))
+
+                #
+                # Apply plugins.
+                #
+                self.action = ''
+                for module in self.modules:
+                    try:
+                        self.logger.debug('Apply plugin (%s).' % (module.__name__, ))
+                        pluginAction = module.restriction(
+                            ldapConn=self.conn,
+                            ldapBaseDn=self.baseDN,
+                            ldapRecipientDn=recipientDn,
+                            ldapRecipientLdif=recipientLdif,
+                            smtpSessionData=map,
+                            logger=self.logger,
+                        )
+
+                        self.logger.debug('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                        if not pluginAction.startswith('DUNNO'):
+                            self.logger.info('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                            return pluginAction
+                    except Exception, e:
+                        self.logger.debug('Error while apply plugin (%s): %s' % (module, str(e)))
+
+            else:
+                # No plugins available.
+                return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['defer']
+
+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+from libs import SMTP_ACTIONS
+
+
+class SQLModeler:
+    def __init__(self, cfg, logger):
+        self.cfg = cfg
+        self.logger = logger
+
+        # Backend
+        self.backend = self.cfg.get('general', 'backend', 'mysql')
+
+        if self.backend == 'mysql':
+            import MySQLdb
+            try:
+                db = MySQLdb.connect(
+                    host=self.cfg.get('sql', 'server', 'localhost'),
+                    db=self.cfg.get('sql', 'db', 'vmail'),
+                    user=self.cfg.get('sql', 'user', 'vmail'),
+                    passwd=self.cfg.get('sql', 'password'),
+                )
+                self.cursor = db.cursor()
+            except Exception, e:
+                self.logger.error("Error while creating database connection: %s" % str(e))
+        elif self.backend == 'pgsql':
+            import psycopg2
+            try:
+                db = psycopg2.connect(
+                    host=self.cfg.get('sql', 'server', 'localhost'),
+                    port=self.cfg.get('sql', 'port', '5432'),
+                    database=self.cfg.get('sql', 'db', 'vmail'),
+                    user=self.cfg.get('sql', 'user', 'vmail'),
+                    password=self.cfg.get('sql', 'password'),
+                )
+                self.cursor = db.cursor()
+            except Exception, e:
+                self.logger.error("Error while creating database connection: %s" % str(e))
+        else:
+            return SMTP_ACTIONS['default']
+
+    def __del__(self):
+        try:
+            self.cursor.close()
+            self.logger.debug('Closed SQL connection.')
+        except Exception, e:
+            self.logger.debug('Error while closing connection: %s' % str(e))
+
+    def handle_data(self, map):
+        if 'sender' in map.keys() and 'recipient' in map.keys():
+            if len(map['sender']) < 6:
+                # Not a valid email address.
+                return 'DUNNO'
+
+            # Get plugin module name and convert plugin list to python list type.
+            self.plugins = self.cfg.get('sql', 'plugins', '')
+            self.plugins = [v.strip() for v in self.plugins.split(',')]
+
+            # Get sender, recipient.
+            # Sender/recipient are used almost in all plugins, so store them
+            # a dict and pass to plugins.
+            senderReceiver = {
+                'sender': map['sender'],
+                'recipient': map['recipient'],
+                'sender_domain': map['sender'].split('@')[-1],
+                'recipient_domain': map['recipient'].split('@')[-1],
+            }
+
+            if len(self.plugins) > 0:
+                #
+                # Import plugin modules.
+                #
+                self.modules = []
+
+                # Load plugin module.
+                for plugin in self.plugins:
+                    try:
+                        self.modules.append(__import__(plugin))
+                    except ImportError:
+                        # Print error message if plugin module doesn't exist.
+                        # Use self.logger.info to let admin know this critical error.
+                        self.logger.info('Error: plugin %s.py not exist.' % plugin)
+                    except Exception, e:
+                        self.logger.debug('Error while importing plugin module (%s): %s' % (plugin, str(e)))
+
+                #
+                # Apply plugins.
+                #
+                self.action = ''
+                for module in self.modules:
+                    try:
+                        self.logger.debug('Apply plugin (%s).' % (module.__name__, ))
+                        pluginAction = module.restriction(
+                            dbConn=self.cursor,
+                            senderReceiver=senderReceiver,
+                            smtpSessionData=map,
+                            logger=self.logger,
+                        )
+
+                        self.logger.debug('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                        if not pluginAction.startswith('DUNNO'):
+                            self.logger.info('Response from plugin (%s): %s' % (module.__name__, pluginAction))
+                            return pluginAction
+                    except Exception, e:
+                        self.logger.debug('Error while apply plugin (%s): %s' % (module, str(e)))
+
+            else:
+                # No plugins available.
+                return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['defer']
+

plugins-rr/__init__.py

Empty file added.

plugins-rr/ldap_expired_password.py

+# Author:   Zhang Huangbin <zhb _at_ iredmail.org>
+# Purpose:  Force user to change account password in 90 days.
+
+import datetime
+from libs import SMTP_ACTIONS
+
+# Force mail user to change password in how many days. Default is 90.
+EXPIRED_DAYS = 90
+
+def restriction(smtpSessionData, ldapSenderLdif, **kargs):
+    # Check password last change days
+    last_changed_day = int(ldapSenderLdif.get('shadowLastChange', [0])[0])
+
+    # Convert today to shadowLastChange
+    today = datetime.date.today()
+    changed_days_of_today = (datetime.date(today.year, today.month, today.day) - datetime.date(1970, 1, 1)).days
+
+    if (last_changed_day + EXPIRED_DAYS) < changed_days_of_today:
+        return 'REJECT Password expired, please change the password before sending email.'
+
+    return SMTP_ACTIONS['default']
+

plugins-rr/ldap_recipient_restrictions.py

+# Author:   Zhang Huangbin <zhb _at_ iredmail.org>
+# Date:     2010-04-20
+# Purpose:  Per-user whitelist/blacklist for recipient restrictions.
+#           Bypass all whitelisted recipients, reject all blacklisted recipients.
+
+# ------------- Addition configure required ------------
+# * In postfix main.cf:
+#
+#   smtpd_sender_restrictions =
+#           check_policy_service inet:127.0.0.1:7778,
+#           [YOUR OTHER RESTRICTIONS HERE]
+#
+#   Here, ip address '127.0.0.1' and port number '7778' are set in iRedAPD-RR
+#   config file: iredapd-rr.ini.
+# ------------------------------------------------------
+
+# Value of mailWhitelistRecipient and mailBlacklistRecipient:
+#   - Single address:   user@domain.ltd
+#   - Whole domain:     @domain.ltd
+#   - Whole Domain and its sub-domains: @.domain.ltd
+#   - All recipient:       @.
+
+def restriction(smtpSessionData, ldapSenderLdif, **kargs):
+    # Get recipient address.
+    smtpRecipient = smtpSessionData.get('recipient').lower()
+    splited_recipient_domain = str(smtpRecipient.split('@')[-1]).split('.')
+
+    # Get correct domain name and sub-domain name.
+    # Sample recipient domain: sub2.sub1.com.cn
+    #   -> sub2.sub1.com.cn
+    #   -> .sub2.sub1.com.cn
+    #   -> .sub1.com.cn
+    #   -> .com.cn
+    #   -> .cn
+    recipients = ['@.', smtpRecipient, '@' + smtpRecipient.split('@')[-1],]
+    for counter in range(len(splited_recipient_domain)):
+        # Append domain and sub-domain.
+        recipients += ['@.' + '.'.join(splited_recipient_domain)]
+        splited_recipient_domain.pop(0)
+
+    # Get value of mailBlacklistedRecipient, mailWhitelistRecipient.
+    blRecipients = [v.lower()
+            for v in ldapSenderLdif.get('mailBlacklistRecipient', [])
+            ]
+
+    wlRecipients = [v.lower()
+            for v in ldapSenderLdif.get('mailWhitelistRecipient', [])
+            ]
+
+    # Bypass whitelisted recipients if has intersection set.
+    if len(set(recipients) & set(wlRecipients)) > 0:
+        return 'DUNNO'
+
+    # Reject blacklisted recipients if has intersection set.
+    if len(set(recipients) & set(blRecipients)) > 0 or '@.' in blRecipients:
+        return 'REJECT Permission denied'
+
+    # If not matched bl/wl list:
+    return 'DUNNO'

plugins/__init__.py

Empty file added.

plugins/block_amavisd_blacklisted_senders.py

+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+# Priority: whitelist first, then blacklist.
+
+PLUGIN_NAME = 'block_amavisd_blacklisted_senders'
+
+from libs import SMTP_ACTIONS
+
+def restriction(smtpSessionData, ldapRecipientLdif, logger, **kargs):
+    # Get sender address.
+    sender = smtpSessionData.get('sender').lower()
+
+    # Get valid Amavisd sender, sender domain and sub-domain(s).
+    # - Sample user: user@sub2.sub1.com.cn
+    # - Valid Amavisd senders:
+    #   -> user@sub2.sub1.com.cn
+    #   -> @sub2.sub1.com.cn
+    #   -> @.sub2.sub1.com.cn
+    #   -> @.sub1.com.cn
+    #   -> @.com.cn
+    #   -> @.cn
+    splited_sender_domain = str(sender.split('@', 1)[-1]).split('.')
+
+    # Default senders (user@domain.ltd): ['@.', 'user@domain.ltd', @domain.ltd']
+    valid_amavisd_senders = set(['@.', sender, '@'+sender.split('@', 1)[-1],])
+    for counter in range(len(splited_sender_domain)):
+        # Append domain and sub-domain.
+        valid_amavisd_senders.update(['@.' + '.'.join(splited_sender_domain)])
+        splited_sender_domain.pop(0)
+
+    # Get list of amavisBlacklistedSender.
+    blSenders = set([v.lower() for v in ldapRecipientLdif.get('amavisBlacklistSender', [])])
+
+    # Get list of amavisWhitelistSender.
+    wlSenders = set([v.lower() for v in ldapRecipientLdif.get('amavisWhitelistSender', [])])
+
+    logger.debug('(%s) Sender: %s' % (PLUGIN_NAME, sender))
+    logger.debug('(%s) Whitelisted senders: %s' % (PLUGIN_NAME, str(wlSenders)))
+    logger.debug('(%s) Blacklisted senders: %s' % (PLUGIN_NAME, str(blSenders)))
+
+    # Bypass whitelisted senders.
+    if len(valid_amavisd_senders & wlSenders) > 0:
+        return SMTP_ACTIONS['accept']
+
+    # Reject blacklisted senders.
+    if len(valid_amavisd_senders & blSenders) > 0:
+        return 'REJECT Blacklisted'
+
+    # Neither blacklisted nor whitelisted.
+    return 'DUNNO No white-/blacklist records found.'

plugins/ldap_domain_wblist.py

+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+# ----------------------------------------------------------------------------
+# This plugin is used for per-domain white-/blacklist.
+# ----------------------------------------------------------------------------
+
+PLUGIN_NAME = 'ldap_domain_wblist'
+
+def restriction(ldapConn, ldapBaseDn, smtpSessionData, logger, **kargs):
+    sender = smtpSessionData['sender'].lower()
+    splitedSenderDomain = str(sender.split('@')[-1]).split('.')
+
+    #filterOfSender = '(domainWhitelistSender=%s)' % (sender,)
+    filterOfSenders = ''
+    listOfRestrictedSenders = [sender, '@'+sender.split('@')[-1],]
+    for counter in range(len(splitedSenderDomain)):
+        # Append domain and sub-domain.
+        listOfRestrictedSenders += ['@.' + '.'.join(splitedSenderDomain)]
+        splitedSenderDomain.pop(0)
+
+    for i in listOfRestrictedSenders:
+        filterOfSenders += '(domainWhitelistSender=%s)(domainBlacklistSender=%s)' % (i, i,)
+
+    recipient = smtpSessionData['recipient'].lower()
+    recipientDomain = recipient.split('@')[-1]
+
+    logger.debug('(%s) Sender: %s' % (PLUGIN_NAME, sender))
+    logger.debug('(%s) Recipient: %s' % (PLUGIN_NAME, recipient))
+
+    # Query ldap to get domain dn, with domain alias support.
+    try:
+        resultDnOfDomain = ldapConn.search_s(
+            ldapBaseDn,
+            1,                  # 1 = ldap.SCOPE_ONELEVEL
+            '(|(domainName=%s)(domainAliasName=%s))' % (recipientDomain, recipientDomain),
+            ['dn'],
+        )
+        dnOfRecipientDomain = resultDnOfDomain[0][0]
+        logger.debug('(%s) DN of recipient domain: %s' % (PLUGIN_NAME, dnOfRecipientDomain))
+    except Exception, e:
+        return 'DUNNO Error while fetching domain dn: %s' % (str(e))
+
+    # Get list of restricted ip addresses.
+    senderIP = smtpSessionData['client_address']
+    (ipf1, ipf2, ipf3, ipf4) = senderIP.split('.')
+    listOfRestrictedIPAddresses = [
+        senderIP,                           # xx.xx.xx.xx
+        '.'.join([ipf1, '%.%', ipf4]),      # xx.%.%.xx
+        '.'.join([ipf1, '%', ipf3, ipf4]),  # xx.%.xx.xx
+        '.'.join([ipf1, '%', ipf3, '%']),   # xx.%.xx.%
+        '.'.join([ipf1, '%.%.%']),          # xx.%.%.%
+        '.'.join([ipf1, ipf2, '%', ipf4]),  # xx.xx.%.xx
+        '.'.join([ipf1, ipf2, '%.%']),      # xx.xx.%.%
+        '.'.join([ipf1, ipf2, ipf3, '%']),  # xx.xx.xx.%
+        '%.%.%.%',                          # %.%.%.% Matches all IP addresses.
+    ]
+
+    filterOfIPAddr = ''
+    for i in listOfRestrictedIPAddresses:
+        filterOfIPAddr += '(domainWhitelistIP=%s)(domainBlacklistIP=%s)' % (i, i,)
+
+    # Generate final search filter.
+    filter = '(&(objectClass=mailDomain)(|(domainName=%s)(domainAliasName=%s))(|%s))' % (
+        recipientDomain,
+        recipientDomain,
+        filterOfSenders + filterOfIPAddr,
+    )
+
+    try:
+        resultWblists = ldapConn.search_s(
+            dnOfRecipientDomain,    # Base dn.
+            0,                      # Search scope. 0 = ldap.SCOPE_BASE
+            filter,                 # Search filter.
+            ['domainWhitelistIP', 'domainWhitelistSender', 'domainBlacklistIP', 'domainBlacklistSender', ],
+        )
+
+        if len(resultWblists) == 0:
+            # No white/blacklist available.
+            return 'DUNNO No white-/blacklist found.'
+
+        ###################
+        # Whitelist first.
+        #
+        whitelistedSenders = resultWblists[0][1].get('domainWhitelistSender', [])
+        whitelistedIPAddresses = resultWblists[0][1].get('domainWhitelistIP', [])
+
+        logger.debug('(%s) Whitelisted senders: %s' % (PLUGIN_NAME, ', '.join(whitelistedSenders)))
+        logger.debug('(%s) Whitelisted IP addresses: %s' % (PLUGIN_NAME, ', '.join(whitelistedIPAddresses)))
+
+        if len(set(listOfRestrictedSenders) & set(whitelistedSenders)) > 0 or \
+           len(set(listOfRestrictedIPAddresses) & set(whitelistedIPAddresses)) > 0:
+            return 'DUNNO Whitelisted.'
+
+        ###################
+        # Blacklist.
+        #
+        blacklistedSenders = resultWblists[0][1].get('domainBlacklistSender', [])
+        blacklistedIPAddresses = resultWblists[0][1].get('domainBlacklistIP', [])
+
+        logger.debug('(%s) Blacklisted senders: %s' % (PLUGIN_NAME, ', '.join(blacklistedSenders)))
+        logger.debug('(%s) Blacklisted IP addresses: %s' % (PLUGIN_NAME, ', '.join(blacklistedIPAddresses)))
+
+        if len(set(listOfRestrictedSenders) & set(blacklistedSenders)) > 0 or \
+           len(set(listOfRestrictedIPAddresses) & set(blacklistedIPAddresses)) > 0:
+            return 'REJECT Blacklisted'
+
+        return 'DUNNO Not listed in white-/blacklist records.'
+    except Exception, e:
+        # Error while quering LDAP server, return 'DUNNO' instead of rejecting emails.
+        return 'DUNNO Error while fetching white-/blacklist records: %s' % (str(e))

plugins/ldap_maillist_access_policy.py

+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+# ----------------------------------------------------------------------------
+# This plugin is used for mail deliver restriction.
+#
+# Available access policies:
+#   - public:   Unrestricted
+#   - domain:   Only users under same domain are allowed.
+#   - subdomain:    Only users under same domain and sub domains are allowed.
+#   - membersOnly or members:  Only members are allowed.
+#   - moderatorsOnly or moderators:   Only moderators are allowed.
+#   - membersAndModeratorsOnly: Only members and moderators are allowed.
+
+# ----------------------------------------------------------------------------
+
+from libs import SMTP_ACTIONS
+
+PLUGIN_NAME = 'ldap_maillist_access_policy'
+
+def __get_allowed_senders(ldapConn, ldapBaseDn, listDn, sender, recipient, policy, logger, *kw, **kargs):
+    """return search_result_list_based_on_access_policy"""
+
+    logger.debug('(%s) Get allowed senders...' % (PLUGIN_NAME))
+
+    recipient_domain = recipient.split('@', 1)[-1]
+
+    # Set base dn as domain dn.
+    domaindn = 'domainName=' + recipient_domain + ',' + ldapBaseDn
+
+    # Default search scope. 2==ldap.SCOPE_SUBTREE
+    searchScope = 2
+
+    # Set search filter, attributes based on policy.
+    # Override base dn, scope if necessary.
+    if policy in ['membersonly', 'members']:
+        basedn = domaindn
+        # Filter: get mail list members.
+        searchFilter = "(&(|(objectclass=mailUser)(objectClass=mailExternalUser))(accountStatus=active)(memberOfGroup=%s))" % (recipient, )
+
+        # Get both mail and shadowAddress.
+        searchAttrs = ['mail', 'shadowAddress',]
+
+    elif policy in ['allowedonly', 'moderatorsonly', 'moderators']:
+        # Get mail list moderators.
+        basedn = listDn
+        searchScope = 0     # Use ldap.SCOPE_BASE to improve performance.
+        searchFilter = "(&(objectclass=mailList)(mail=%s))" % (recipient, )
+        searchAttrs = ['listAllowedUser']
+
+    else:
+        basedn = domaindn
+        # Policy: policy==membersAndModeratorsOnly or not set.
+        # Filter used to get both members and moderators.
+        searchFilter = "(|(&(|(objectClass=mailUser)(objectClass=mailExternalUser))(memberOfGroup=%s))(&(objectclass=mailList)(mail=%s)))" % (recipient, recipient, )
+        searchAttrs = ['mail', 'shadowAddress', 'listAllowedUser',]
+
+    logger.debug('(%s) base dn: %s' % (PLUGIN_NAME, basedn))
+    logger.debug('(%s) search scope: %s' % (PLUGIN_NAME, searchScope))
+    logger.debug('(%s) search filter: %s' % (PLUGIN_NAME, searchFilter))
+    logger.debug('(%s) search attributes: %s' % (PLUGIN_NAME, ', '.join(searchAttrs)))
+
+    try:
+        result = ldapConn.search_s(basedn, searchScope, searchFilter, searchAttrs)
+        userList = []
+        for obj in result:
+            for k in searchAttrs:
+                if k in obj[1].keys():
+                    # Example of result data:
+                    # [('dn', {'listAllowedUser': ['user@domain.ltd']})]
+                    userList += obj[1][k]
+                else:
+                    pass
+
+        # Exclude mail list itself.
+        if recipient in userList:
+            userList.remove(recipient)
+
+        logger.debug('(%s) search result: %s' % (PLUGIN_NAME, str(userList)))
+
+        # Query once more to get 'shadowAddress'.
+        if len(userList) > 0 and (policy == 'allowedonly' or policy == 'moderatorsonly'):
+            logger.debug('(%s) Addition query to get user aliases...' % (PLUGIN_NAME))
+
+            basedn = 'ou=Users,' + domaindn
+            searchFilter = '(&(objectClass=mailUser)(enabledService=shadowaddress)(|'
+            for i in userList:
+                searchFilter += '(mail=%s)' % i
+            searchFilter += '))'
+
+            searchAttrs = ['shadowAddress',]
+
+            logger.debug('(%s) base dn: %s' % (PLUGIN_NAME, basedn))
+            logger.debug('(%s) search scope: 2 (ldap.SCOPE_SUBTREE)' % (PLUGIN_NAME))
+            logger.debug('(%s) search filter: %s' % (PLUGIN_NAME, searchFilter))
+            logger.debug('(%s) search attributes: %s' % (PLUGIN_NAME, ', '.join(searchAttrs)))
+
+            try:
+                resultOfShadowAddresses = ldapConn.search_s(
+                    'ou=Users,'+domaindn,
+                    2,  # ldap.SCOPE_SUBTREE
+                    searchFilter,
+                    ['mail', 'shadowAddress',],
+                )
+
+                for obj in resultOfShadowAddresses:
+                    for k in searchAttrs:
+                        if k in obj[1].keys():
+                            # Example of result data:
+                            # [('dn', {'listAllowedUser': ['user@domain.ltd']})]
+                            userList += obj[1][k]
+                        else:
+                            pass
+
+                logger.debug('(%s) final result: %s' % (PLUGIN_NAME, str(userList)))
+
+            except Exception, e:
+                logger.debug(str(e))
+
+        return userList
+    except Exception, e:
+        logger.debug('(%s) Error: %s' % (PLUGIN_NAME, str(e)))
+        return []
+
+def restriction(ldapConn, ldapBaseDn, ldapRecipientDn, ldapRecipientLdif, smtpSessionData, logger, **kargs):
+    # Return if recipient is not a mail list object.
+    if 'maillist' not in [v.lower() for v in ldapRecipientLdif['objectClass']]:
+        return 'DUNNO Not a mail list account.'
+
+    sender = smtpSessionData['sender'].lower()
+    sender_domain = sender.split('@')[-1]
+
+    recipient = smtpSessionData['recipient'].lower()
+    recipient_domain = recipient.split('@')[-1]
+    recipient_alias_domains = []
+
+    policy = ldapRecipientLdif.get('accessPolicy', ['public'])[0].lower()
+
+    if policy in ['domain', 'subdomain',]:
+        try:
+            qr = ldapConn.search_s(
+                ldapBaseDn,
+                1, # 1 == ldap.SCOPE_ONELEVEL
+                "(&(objectClass=mailDomain)(|(domainName=%s)(domainAliasName=%s)))" % (recipient_domain, recipient_domain),
+                ['domainName', 'domainAliasName',]
+            )
+            if len(qr) > 0:
+                recipient_alias_domains = qr[0][1].get('domainName', []) + qr[0][1].get('domainAliasName', [])
+        except Exception, e:
+            logger.debug('(%s) Error while fetch domainAliasName: %s' % (PLUGIN_NAME, str(e),))
+
+        logger.debug('(%s) Recipient domain and alias domains: %s' % (PLUGIN_NAME, ','.join(recipient_alias_domains)))
+
+    logger.debug('(%s) %s -> %s, policy: %s' % (PLUGIN_NAME, sender, recipient, policy))
+
+    if policy == 'public':
+        # No restriction.
+        return 'DUNNO Access policy: public.'
+    elif policy == "domain":
+        # Bypass all users under the same domain.
+        if sender_domain in recipient_alias_domains:
+            return 'DUNNO Access policy: domain'
+        else:
+            return SMTP_ACTIONS['reject']
+    elif policy == "subdomain":
+        # Bypass all users under the same domain and sub domains.
+        returned = False
+        for d in recipient_alias_domains:
+            if sender.endswith(d) or sender.endswith('.' + d):
+                return 'DUNNO Access policy: subdomain (%s)' % (d)
+
+        if returned is False:
+            return SMTP_ACTIONS['reject']
+    else:
+        # Handle other access policies: membersOnly, allowedOnly, membersAndModeratorsOnly.
+        allowedSenders = __get_allowed_senders(
+            ldapConn=ldapConn,
+            ldapBaseDn=ldapBaseDn,
+            listDn=ldapRecipientDn,
+            sender=sender,
+            recipient=recipient,
+            policy=policy,
+            logger=logger,
+        )
+
+        if sender.lower() in [v.lower() for v in allowedSenders]:
+            return 'DUNNO Allowed sender.'
+        else:
+            return SMTP_ACTIONS['reject']

plugins/sql_alias_access_policy.py

+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+# Purpose: Apply access policy on sender while recipient is an mail alias.
+
+# Available access policies:
+#   - public:   Unrestricted
+#   - domain:   Only users under same domain are allowed.
+#   - subdomain:    Only users under same domain and sub domains are allowed.
+#   - membersOnly:  Only members are allowed.
+#   - moderatorsOnly:   Only moderators are allowed.
+#   - membersAndModeratorsOnly: Only members and moderators are allowed.
+from web import sqlquote
+from libs import SMTP_ACTIONS
+
+PLUGIN_NAME = 'sql_alias_access_policy'
+
+# Policies. MUST be defined in lower case.
+POLICY_PUBLIC = 'public'
+POLICY_DOMAIN = 'domain'
+POLICY_SUBDOMAIN = 'subdomain'
+POLICY_MEMBERSONLY = 'membersonly'
+POLICY_MODERATORSONLY = 'moderatorsonly'
+POLICY_ALLOWEDONLY = 'allowedonly'      # Same as @POLICY_MODERATORSONLY
+POLICY_MEMBERSANDMODERATORSONLY = 'membersandmoderatorsonly'
+
+def restriction(dbConn, senderReceiver, smtpSessionData, logger, **kargs):
+
+    sql = '''SELECT accesspolicy, goto, moderators
+            FROM alias
+            WHERE
+                address=%s
+                AND address <> goto
+                AND domain=%s
+                AND active=1
+            LIMIT 1
+    ''' % (sqlquote(senderReceiver.get('recipient')),
+           sqlquote(senderReceiver.get('recipient_domain')),
+          )
+    logger.debug('SQL: %s' % sql)
+
+    dbConn.execute(sql)
+    sqlRecord = dbConn.fetchone()
+    logger.debug('SQL Record: %s' % str(sqlRecord))
+
+    # Recipient account doesn't exist.
+    if sqlRecord is None:
+        return 'DUNNO Not an alias account.'
+
+    policy = str(sqlRecord[0]).lower()
+
+    members = [str(v.lower()) for v in str(sqlRecord[1]).split(',')]
+    moderators = [str(v.lower()) for v in str(sqlRecord[2]).split(',')]
+
+    logger.debug('(%s) policy: %s' % (PLUGIN_NAME, policy))
+    logger.debug('(%s) members: %s' % (PLUGIN_NAME, ', '.join(members)))
+    logger.debug('(%s) moderators: %s' % (PLUGIN_NAME, ', '.join(moderators)))
+
+    if not len(policy) > 0:
+        return 'DUNNO No access policy defined.'
+
+    if policy == POLICY_PUBLIC:
+        # Return if no access policy available or policy is @POLICY_PUBLIC.
+        return 'DUNNO'
+    elif policy == POLICY_DOMAIN:
+        # Bypass all users under the same domain.
+        if senderReceiver['sender_domain'] == senderReceiver['recipient_domain']:
+            return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['reject']
+    elif policy == POLICY_SUBDOMAIN:
+        # Bypass all users under the same domain or sub domains.
+        if senderReceiver['sender'].endswith(senderReceiver['recipient_domain']) or \
+           senderReceiver['sender'].endswith('.' + senderReceiver['recipient_domain']):
+            return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['reject']
+    elif policy == POLICY_MEMBERSONLY:
+        # Bypass all members.
+        if senderReceiver['sender'] in members:
+            return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['reject']
+    elif policy == POLICY_MODERATORSONLY or policy == POLICY_ALLOWEDONLY:
+        # Bypass all moderators.
+        if senderReceiver['sender'] in moderators:
+            return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['reject']
+    elif policy == POLICY_MEMBERSANDMODERATORSONLY:
+        # Bypass both members and moderators.
+        if senderReceiver['sender'] in members or senderReceiver['sender'] in moderators:
+            return 'DUNNO'
+        else:
+            return SMTP_ACTIONS['reject']
+    else:
+        # Bypass all if policy is not defined in this plugin.
+        return 'DUNNO Policy is not defined in plugin (%s): %s.' % (PLUGIN_NAME, policy)

plugins/sql_user_restrictions.py

+# Author: Zhang Huangbin <zhb _at_ iredmail.org>
+
+# Purpose: Per-user send/receive restrictions.
+#
+# Required SQL columns of table `vmail.mailbox`.
+#   - mailbox.allowedrecipients: Allow user to send TO listed recipients
+#   - mailbox.rejectedrecipients: Reject emails sent TO listed recipients
+#   - mailbox.allowedsenders: Accept emails FROM listed senders
+#   - mailbox.rejectedsenders: Reject emails FROM listed senders
+#
+# Valid sender/recipient addresses:
+#
+#   - .*:           all addresses (user, domain, sub-domain)
+#   - domain.com:   single domain
+#   - .domain.com:  single domain and its all sub-domains
+#   - user@domain.com:  single email address
+
+from web import sqlquote
+from libs import SMTP_ACTIONS
+
+PLUGIN_NAME = 'sql_user_restrictions'
+
+def restriction(dbConn, senderReceiver, smtpSessionData, logger, **kargs):
+    # Get restriction rules for sender
+    sql = '''
+        SELECT
+            allowedrecipients, rejectedrecipients,
+            allowedsenders, rejectedsenders
+        FROM mailbox
+        WHERE username=%s
+        LIMIT 1
+    ''' % sqlquote(senderReceiver['sender'])
+    logger.debug('SQL to get restriction rules of sender (%s): %s' % (senderReceiver['sender'], sql))
+
+    dbConn.execute(sql)
+    sql_record = dbConn.fetchone()
+    logger.debug('Returned SQL Record: %s' % str(sql_record))
+
+    # Sender account exists, perform recipient restrictions
+    if sql_record:
+        allowed_recipients, rejected_recipients, allowed_senders, rejected_senders = sql_record
+
+        # If it does have restrictions
+        if not allowed_recipients and not rejected_recipients:
+            logger.debug('No restrictions of sender.')
+        else:
+            # Allowed first
+            # single recipient, domain, sub-domain, catch-all
+            all_allowed_recipients = [s.lower().strip() for s in allowed_recipients.split(',')]
+            logger.debug('All allowed recipient: %s' % str(all_allowed_recipients))
+
+            if all_allowed_recipients:
+                if senderReceiver['recipient'] in all_allowed_recipients \
+                   or senderReceiver['recipient_domain'] in all_allowed_recipients \
+                   or '.' + senderReceiver['recipient_domain'] in all_allowed_recipients \
+                   or '*' in all_allowed_recipients:
+                    return SMTP_ACTIONS['accept']
+
+            all_rejected_recipients = [s.lower().strip() for s in rejected_recipients.split(',')]
+            logger.debug('All rejected recipient: %s' % str(all_rejected_recipients))
+
+            if all_rejected_recipients:
+                if senderReceiver['recipient'] in all_rejected_recipients \
+                   or senderReceiver['recipient_domain'] in all_rejected_recipients \
+                   or '.' + senderReceiver['recipient_domain'] in all_rejected_recipients \
+                   or '*' in all_rejected_recipients:
+                    return SMTP_ACTIONS['reject']
+
+    # Get restriction rules for recipient
+    # Don't perform another SQL query if sender == recipient
+    if senderReceiver['sender'] != senderReceiver['recipient']:
+        sql = '''
+            SELECT
+                allowedrecipients, rejectedrecipients,
+                allowedsenders, rejectedsenders
+            FROM mailbox
+            WHERE username=%s
+            LIMIT 1
+        ''' % sqlquote(senderReceiver['recipient'])
+        logger.debug('SQL to get restriction rules of recipient (%s): %s' % (senderReceiver['recipient'], sql))
+
+        dbConn.execute(sql)
+        sql_record = dbConn.fetchone()
+        logger.debug('Returned SQL Record: %s' % str(sql_record))
+
+    # Recipient account exists, perform sender restrictions
+    if sql_record:
+        allowed_recipients, rejected_recipients, allowed_senders, rejected_senders = sql_record
+
+        # If it does have restrictions
+        if not allowed_senders and not rejected_senders:
+            logger.debug('No restrictions of recipient.')
+        else:
+            # Allowed first
+            # single recipient, domain, sub-domain, catch-all
+            all_allowed_senders = [s.lower().strip() for s in allowed_senders.split(',')]
+            logger.debug('All allowed senders: %s' % str(all_allowed_senders))
+
+            if all_allowed_senders:
+                if senderReceiver['sender'] in all_allowed_senders \
+                   or senderReceiver['sender_domain'] in all_allowed_senders \
+                   or '.' + senderReceiver['sender_domain'] in all_allowed_senders \
+                   or '.*' in all_allowed_senders:
+                    return SMTP_ACTIONS['accept']
+
+            all_rejected_senders = [s.lower().strip() for s in rejected_senders.split(',')]
+            logger.debug('All rejected senders: %s' % str(all_rejected_senders))
+
+            if all_rejected_senders:
+                if senderReceiver['sender'] in all_rejected_senders \
+                   or senderReceiver['sender_domain'] in all_rejected_senders \
+                   or '.' + senderReceiver['sender_domain'] in all_rejected_senders \
+                   or '*' in all_rejected_senders:
+                    return SMTP_ACTIONS['reject']
+
+    return SMTP_ACTIONS['default']

rc_scripts/iredapd-rr.debian

 ### END INIT INFO
 
 PROG='iredapd'
-BINPATH='/opt/iredapd/src/iredapd-rr.py'
+BINPATH='/opt/iredapd/iredapd-rr.py'
 CONFIG='/opt/iredapd/etc/iredapd-rr.ini'
 PIDFILE='/var/run/iredapd-rr.pid'
 

rc_scripts/iredapd-rr.freebsd

 name='iredapd'
 rcvar=`set_rcvar`
 
-BINPATH='/opt/iredapd/src/iredapd-rr.py'
+BINPATH='/opt/iredapd/iredapd-rr.py'
 CONFIG='/opt/iredapd/etc/iredapd-rr.ini'
 pidfile='/var/run/iredapd-rr.pid'
 

rc_scripts/iredapd-rr.openbsd

 #!/bin/sh
 
-daemon='python /opt/iredapd/src/iredapd-rr.py /opt/iredapd/etc/iredapd-rr.ini'
+daemon='python /opt/iredapd/iredapd-rr.py /opt/iredapd/etc/iredapd-rr.ini'
 
 . /etc/rc.d/rc.subr
 

rc_scripts/iredapd-rr.opensuse

 ### END INIT INFO
 
 PROG='iredapd'
-BINPATH='/opt/iredapd/src/iredapd-rr.py'
+BINPATH='/opt/iredapd/iredapd-rr.py'
 CONFIG='/opt/iredapd/etc/iredapd-rr.ini'
 PIDFILE='/var/run/iredapd-rr.pid'
 

rc_scripts/iredapd-rr.rhel

 ### END INIT INFO
 
 PROG='iredapd'
-BINPATH='/opt/iredapd/src/iredapd-rr.py'
+BINPATH='/opt/iredapd/iredapd-rr.py'
 CONFIG='/opt/iredapd/etc/iredapd-rr.ini'
 PIDFILE='/var/run/iredapd-rr.pid'
 

rc_scripts/iredapd.debian

 ### END INIT INFO
 
 PROG='iredapd'
-BINPATH='/opt/iredapd/src/iredapd.py'
+BINPATH='/opt/iredapd/iredapd.py'
 CONFIG='/opt/iredapd/etc/iredapd.ini'
 PIDFILE='/var/run/iredapd.pid'
 

rc_scripts/iredapd.freebsd

 name='iredapd'
 rcvar=`set_rcvar`
 
-BINPATH='/opt/iredapd/src/iredapd.py'
+BINPATH='/opt/iredapd/iredapd.py'
 CONFIG='/opt/iredapd/etc/iredapd.ini'
 pidfile='/var/run/iredapd.pid'
 

rc_scripts/iredapd.gentoo

 #!/sbin/runscript
 
-prog='/opt/iredapd/src/iredapd.py'
+prog='/opt/iredapd/iredapd.py'
 progname="iRedAPD"
 
 CONFIG='/opt/iredapd/etc/iredapd.ini'

rc_scripts/iredapd.openbsd

 #!/bin/sh
 
-daemon='python /opt/iredapd/src/iredapd.py /opt/iredapd/etc/iredapd.ini'
+daemon='python /opt/iredapd/iredapd.py /opt/iredapd/etc/iredapd.ini'
 
 . /etc/rc.d/rc.subr