Zhang Huangbin avatar Zhang Huangbin committed 6143b46

Import iredapd: mail list access policy daemon.

Comments (0)

Files changed (9)

+iRedAPD-1.0:
+    * Initialize version.
+- Requirements:
+
+    # Python 2.4+: http://www.python.org/
+    # python-daemon:
+    # python-ldap: http://www.python-ldap.org/
+
+- Install iredapd.
+
+    - Install required python modules:
+
+        # easy_install daemon python-ldap
+
+    - Copy iredapd to /opt:
+
+        # cp rc_scripts/iredapd.rhel /etc/init.d/iredapd
+
+- Run iredapd:
+
+    # bash bin/iredapd etc/iredapd.ini
+
+- Postfix configuration:
+
+    In postfix main.cf, modify 'smtpd_recipient_restrictions' setting:
+
+        smtpd_recipient_restrictions =
+            ...
+            permit_mynetworks,
+            reject_unauth_destination,                  # First
+            check_policy_service inet:127.0.0.1:7777,   # Second
+            permit_sasl_authenticated,                  # Third
+            ...
+
+    Note:
+        - iRedMail has 'permit_sasl_authenticated' in front of
+          'reject_unauth_destination' by default, you must swap them.
+        - 'check_policy_service inet:127.0.0.1:7777' MUST stay in
+          front of 'permit_sasl_authenticated'.
+        - Restart postfix to make it work.
+
+- Access policies:
+    - public        # Unrestricted.
+    - domain        # Restricted to users under same domain.
+    - membersOnly    # Only members are allowed.
+    - allowedOnly   # Only allowed addresses are allowed.
+
+Authors & Contributors
+----------------------
+
+Zhang Huangbin (michaelbibby@gmail.com): Core developer and maintainer
+
+About
+-----
+
+iRedAPD is a Mailing List Access Policyd Daemon which works as a
+Postfix Policy Access Delegation deamon and uses LDAP to store access 
+policy.
+
+License
+-------
+
+iRedAPD is based on mlapd (http://code.google.com/p/mlapd) which is a
+GPLv2 licensed open source software, so iRedAPD is released under
+the same license - GPLv2.
+
+Note: file src/daemon.py is released under its own license, shipped
+here for easy deploying.
+
+Requirments:
+------------
+
+* Python ( >= 2.4 )
+* python-ldap ( >= 2.2.0 )
+
+Postfix and LDAP are not needed on the same server machine where you
+are going to install iRedAPD, but they have to be reached by iRedAPD to
+make it working!
+* RC script.
+* Check log type and log into correct logger handler.
+* Per-maillist restrictions:
+    - Verify recipient address is mail list.
+    - Show sender & recipient in logging.info.
+    - Handle multiple access policies.
+    - Always convert value of access policy to lower case string.
+    - Set filter attributes in config file instead of hardcode here
+    - If access policy is 'memberOnly', how about a fake email address?
+    - Set logging level in config file.
+
+* Per-user restrictions:
+    - http://www.iredmail.org/forum/topic211-block-sender-or-recipient.html
+    - http://www.iredmail.org/forum/topic235-restrict-sending-to-certain-domains.html
+[general]
+listen_addr     = 127.0.0.1
+listen_port     = 7777
+
+# Background/daemon mode: yes, no.
+# Detach iredapd from terminal. Enable when you're happy
+# that things are working as expected.
+run_as_daemon   = yes
+
+# Path to pid file.
+pid_file        = /var/run/iredapd.pid
+
+# Log type: file, mysql.
+# 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.log
+
+log_sql_server  = 127.0.0.1
+log_sql_port    = 3306
+log_sql_dbname  = iredadmin
+log_sql_dbuser  = iredadmin
+log_sql_dbpass  = your_passwd
+
+# Log level: info, warning, error, debug.
+# 'info' is recommended for product use.
+log_level       = info
+
+# Default action: bypass, reject.
+# If no access policy returned, use this default action.
+default_action  = bypass
+
+[ldap]
+# LDAP server setting.
+# Uri must starts with ldap:// or ldaps:// (TLS/SSL).
+uri         = ldap://127.0.0.1:389
+binddn      = cn=vmail,dc=iredmail,dc=org
+bindpw      = 5NC4VyRJdws3ounpcKJw9zXu0B8ou6
+basedn      = o=domains,dc=iredmail,dc=org
+
+# Attribute name which used to store mail list access policy
+attr_access_policy  = accessPolicy
+
+# LDAP filter used to search mail list
+# Note: placeholder '%(recipient)s' can be used here, will be
+# replaced by mail list address.
+filter_maillist = (&(mail=%(recipient)s)(objectclass=mailList)(accountStatus=active)(enabledService=mail)(enabledService=deliver))
+
+# LDAP filter used to search members of mail list
+# Note:
+# - placeholder '%(recipient)s' can be used here, will be replacd
+#   by mail list address.
+# - placeholder '%(sender)s' can be used here, will be replaced
+#   by sender address.
+filter_member   = (&(memberOfGroup=%(recipient)s)(objectclass=mailUser)(accountStatus=active)(enabledService=mail)(enabledService=deliver)(mail=%(sender)s))
+
+# LDAP filter used to search allowed users.
+# Note:
+# - placeholder '%(recipient)s' can be used here, will be replacd
+#   by mail list address.
+# - placeholder '%(sender)s' can be used here, will be replaced
+#   by sender address.
+filter_allowed_senders = (&(mail=%(recipient)s)(objectclass=mailList)(accountStatus=active)(enabledService=mail)(enabledService=deliver)(listAllowedUser=%(sender)s))

rc_scripts/iredapd.rhel

+#!/usr/bin/env bash
+
+# chkconfig: - 27 73
+# description: LDAP stands for Lightweight Directory Access Protocol, used \
+#              for implementing the industry standard directory services.
+# processname: slapd
+# config: /etc/openldap/slapd.conf
+# pidfile: /var/run/openldap/slapd.pid
+
+PROG='iredapd'
+BINPATH='/opt/iredapd/src/iredapd.py'
+CONFIG='/opt/iredapd/etc/iredapd.ini'
+PIDFILE='/var/run/iredapd.pid'
+
+. /etc/rc.d/init.d/functions
+
+# Source networking configuration and check that networking is up.
+if [ -r /etc/sysconfig/network ] ; then
+    . /etc/sysconfig/network
+    [ X"${NETWORKING}" == X"no" ] && exit 1
+fi
+
+start() {
+    if [ -f ${PIDFILE} ]; then
+        status -p ${PIDFILE} ${PROG} > /dev/null
+
+        if [ X"$?" == X"0" ]; then
+            echo -n "${PROG} is already running: "
+            echo -n "$(pidof ${PROG})" && success && echo
+        else
+            rm -f ${PIDFILE} >/dev/null 2>&1
+            echo -n "Starting ${PROG}:"
+            python ${BINPATH} ${CONFIG} && success || failure
+            echo
+        fi
+    else
+        echo -n "Starting ${PROG}:"
+        python ${BINPATH} ${CONFIG} && success || failure
+        echo
+    fi
+}
+
+stop() {
+    if [ -e ${PIDFILE} ]; then
+        status -p ${PIDFILE} ${PROG} > /dev/null
+
+        if [ X"$?" == X"0" ]; then
+            echo -n "Stopping ${PROG}:"
+            kill $(cat ${PIDFILE}) && success || failure
+            echo
+            rm -f ${PIDFILE} >/dev/null 2>&1
+        else
+            echo -n "${PROG} is already stopped." && success
+            echo
+            rm -f ${PIDFILE} >/dev/null 2>&1
+        fi
+    else
+        echo -n "${PROG} is already stopped." && success
+        echo
+    fi
+}
+
+case "$1" in
+    start)
+        start
+        RETVAL=$?
+        ;;
+    stop)
+        stop
+        RETVAL=$?
+        ;;
+    status) status -p ${PIDFILE} ${PROG};;
+    restart)
+        stop
+        start
+        ;;
+    *)
+        echo $"Usage: $0 {start|stop|restart|status}"
+        RETVAL=1
+        ;;
+esac
+
+exit $RETVAL

rc_scripts/iredapd.ubuntu

+#!/usr/bin/env bash
+
+#
+### BEGIN INIT INFO
+# Provides:          iredapd 
+# Required-Start:    $network $syslog
+# Required-Stop:     $network $syslog
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Control iredapd daemon.
+### END INIT INFO
+#
+
+PROG='iredapd'
+BINPATH='/opt/iredapd/src/iredapd.py'
+CONFIG='/opt/iredapd/etc/iredapd.ini'
+PIDFILE='/var/run/iredapd.pid'
+
+# Source function library.
+. /lib/lsb/init-functions
+
+start() {
+    start-stop-daemon --start --quiet --pidfile ${PIDFILE} --exec ${BINPATH} -- ${CONFIG} || return 2
+}
+
+stop() {
+    start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile ${PIDFILE} --name ${PROG}
+    RETVAL="$?"
+    [ X"${RETVAL}" == X"2" ] && return 2
+
+    start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile ${PIDFILE} --name ${PROG}
+    [ "$?" = 2 ] && return 2
+
+    # Many daemons don't delete their pidfiles when they exit.
+    rm -f $PIDFILE
+    return "$RETVAL"
+}
+
+case "$1" in
+    start)
+        start
+        RETVAL=$?
+        ;;
+    stop)
+        stop
+        RETVAL=$?
+        ;;
+    status)
+        status_of_proc -p ${PIDFILE} ${PROG} ${PROG} && exit 0 || exit $?
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    *)
+        echo $"Usage: $0 {start|stop|restart|status}"
+        RETVAL=1
+        ;;
+esac
+
+exit $RETVAL
+#!/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
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+# Default daemon parameters.
+# File mode creation mask of the daemon.
+UMASK = 0
+
+# 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()
+    
+        # 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)
+#!/usr/bin/env python
+# encoding: utf-8
+
+# Author: Zhang Huangbin <michaelbibby (at) gmail.com>
+
+import os, os.path
+import sys
+import ConfigParser
+import socket
+import asyncore, asynchat
+import logging
+import ldap
+import daemon
+
+__version__ = "1.0"
+
+ACTION_ACCEPT = "action=OK"
+ACTION_DEFER = "action=DEFER_IF_PERMIT Service temporarily unavailable"
+ACTION_REJECT = 'action=REJECT Not Authorized'
+
+# 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)
+
+ACTION_DEFAULT = cfg.get('general', 'default_action', 'bypass')
+if ACTION_DEFAULT == 'bypass':
+    ACTION_DEFAULT = ACTION_ACCEPT
+elif ACTION_DEFAULT == 'defer':
+    ACTION_DEFAULT = ACTION_DEFER
+elif ACTION_DEFAULT == 'reject':
+    ACTION_DEFAULT = ACTION_REJECT
+
+class apdChannel(asynchat.async_chat):
+    def __init__(self, conn, remoteaddr):
+        asynchat.async_chat.__init__(self, conn)
+        self.buffer = []
+        self.map = {}
+        self.set_terminator('\n')
+        logging.debug("Connect from " + remoteaddr[0])
+
+    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]
+                self.map[key] = value
+        elif len(self.map) != 0:
+            try:
+                modeler = LDAPModeler()
+                result = modeler.handle_data(self.map)
+                logging.debug("result replying: %s." % str(result))
+                if result != None:
+                    action = result
+                else:
+                    action = ACTION_ACCEPT
+            except Exception, e:
+                action = ACTION_DEFAULT
+                logging.debug('Error: %s. Use default action instead: %s' % (str(e), str(action)) )
+            logging.info('%s -> %s, %s' % (self.map['sender'], self.map['recipient'], str(action).split('=')[1] ))
+            self.push(action)
+            self.push('')
+            asynchat.async_chat.handle_close(self)
+            #logging.info("Connection closed")
+        else:
+            action = ACTION_DEFER
+            logging.debug("replying: " + action)
+            self.push(action)
+            self.push('')
+            asynchat.async_chat.handle_close(self)
+            logging.debug("Connection closed")
+
+class apdSocket(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, remoteaddr = self.accept()
+        channel = apdChannel(conn, remoteaddr)
+
+class LDAPModeler:
+    def __init__(self):
+        # 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 = 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 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_access_policy(self, listname):
+        """Get access policy of mail list.
+
+        return (dn_of_mail_list, value_of_access_policy,)"""
+
+        logging.debug('__get_access_policy (list): %s' % listname)
+
+        # Replace 'recipient' setting in config file with mail list address.
+        try:
+            cfg.set('ldap', "recipient", listname)
+        except Exception, e:
+            logging.error("""Error while replacing 'recipient': %s""" % (str(e)) )
+
+        searchBasedn = 'mail=%s,ou=Groups,domainName=%s,%s' % (listname, listname.split('@')[1], self.baseDN)
+        searchScope = ldap.SCOPE_BASE
+        searchFilter = cfg.get('ldap', 'filter_maillist')
+        searchAttrs = cfg.get('ldap', 'attr_access_policy', 'accessPolicy')
+
+        logging.debug('__get_access_policy (searchBasedn): %s' % searchBasedn)
+        logging.debug('__get_access_policy (searchScope): %s' % searchScope)
+        logging.debug('__get_access_policy (searchFilter): %s' % searchFilter)
+        logging.debug('__get_access_policy (searchAttrs): %s' % searchAttrs)
+
+        try:
+            result = self.conn.search_s(searchBasedn, searchScope, searchFilter, [searchAttrs])
+            logging.debug('__get_access_policy (search result): %s' % str(result))
+        except ldap.NO_SUCH_OBJECT:
+            logging.debug('__get_access_policy (not a mail list: %s) Returned (None)' % listname)
+            return (None, None)
+        except Exception, e:
+            logging.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][searchAttrs][0]
+            returnVal = (listdn, listpolicy)
+
+            logging.debug('__get_access_policy (returned): %s' % str(returnVal))
+            return returnVal
+
+    def __get_allowed_senders(self, listdn, listname, listpolicy, sender=None):
+        """return search_result_list_based_on_access_policy"""
+        logging.debug('__get_allowed_senders (listpolicy): %s' % listpolicy)
+
+        # Set sender.
+        if sender is None: sender = ''
+
+        # Replace 'recipient' and 'sender' with email addresses.
+        cfg.set("ldap", "recipient", listname)
+        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
+            searchFilter = cfg.get("ldap", "filter_member")
+            searchAttrs = ['mail']
+        else:
+            baseDN = listdn
+            searchScope = ldap.SCOPE_BASE   # Use SCOPE_BASE to improve performance.
+            searchFilter = cfg.get("ldap", "filter_allowed_senders")
+            searchAttrs = ['listAllowedUser']
+
+        logging.debug('__get_allowed_senders (baseDN): %s' % baseDN)
+        logging.debug('__get_allowed_senders (seachScope): %s' % searchScope)
+        logging.debug('__get_allowed_senders (searchFilter): %s' % searchFilter)
+
+        results_id = self.conn.search(baseDN, searchScope, searchFilter, searchAttrs)
+        while True:
+            result_type, result_data = self.conn.result(results_id, 0)
+
+            # Debug.
+            logging.debug('__get_allowed_senders (result_type): %s' % result_type)
+            logging.debug('__get_allowed_senders (result_data): %s' % result_data)
+
+            if (result_data == []):
+                return None
+            else:
+                if result_type == ldap.RES_SEARCH_ENTRY:
+                    result_dn, result_set = result_data[0]
+
+                    # Debug.
+                    logging.debug('__get_allowed_senders (result_dn): %s' % result_dn)
+                    logging.debug('__get_allowed_senders (result_set): %s' % result_set)
+
+                    for attribute in searchAttrs:
+                        logging.debug('__get_allowed_senders (attribute): %s' % attribute)
+
+                        return result_set[attribute]
+
+    def __get_smtp_action(self, listname, sender):
+        """return smtp_action"""
+        listdn, listpolicy = self.__get_access_policy(listname)
+
+        logging.debug('__get_smtp_action (list_dn): %s' % listdn)
+        logging.debug('__get_smtp_action (listpolicy): %s' % listpolicy)
+        logging.debug('__get_smtp_action (sender): %s' % sender)
+
+        if listdn is None or listpolicy is None:
+            return None
+        else:
+            if listpolicy == "public":
+                # No restriction.
+                return ACTION_ACCEPT
+            elif listpolicy == "domain":
+                # Allow all users under the same domain.
+                if sender.split('@')[1] == listname.split('@')[1]:
+                    return ACTION_ACCEPT
+                else:
+                    return ACTION_REJECT
+            elif listpolicy == "allowedOnly":
+                # Bypass allowed users only.
+                allowed_senders = self.__get_allowed_senders(listdn, listname, 'allowedOnly', sender)
+
+                logging.debug('__get_smtp_action (allowed_senders): %s (allowedOnly)' % allowed_senders )
+
+                if allowed_senders is not None:
+                    addresses = set(allowed_senders)    # Remove duplicate addresses.
+                    if sender in addresses:
+                        return ACTION_ACCEPT
+                    else:
+                        return ACTION_REJECT
+                else:
+                    return ACTION_REJECT
+            elif listpolicy == "membersOnly":
+                allowed_senders = self.__get_allowed_senders(listdn, listname, 'membersOnly', sender)
+
+                logging.debug('__get_smtp_action (allowed_senders): %s (membersOnly)' % allowed_senders)
+
+                if allowed_senders is not None:
+                    addresses = set(allowed_senders)
+                    if sender in addresses:
+                        return ACTION_ACCEPT
+                    else:
+                        return ACTION_REJECT
+                else:
+                    #return ACTION_DEFER
+                    return ACTION_REJECT
+
+    def handle_data(self, map):
+        if map.has_key("sender") and map.has_key("recipient"):
+            sender = map["sender"]
+            recipient = map["recipient"]
+            action = self.__get_smtp_action(recipient, sender)
+            return action
+        else:
+            return ACTION_DEFER
+
+def main():
+    # Chroot in current directory.
+    try:
+        os.chdir(os.path.dirname(__file__))
+    except:
+        pass
+
+    # 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.
+    socketDaemon = apdSocket((listen_addr, listen_port))
+
+    # Run this program as daemon.
+    if run_as_daemon == 'yes':
+        daemon.daemonize()
+
+    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()
+
+        # Starting loop.
+        asyncore.loop()
+    except KeyboardInterrupt:
+        pass
+
+if __name__ == '__main__':
+    main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.