Commits

Zhang Huangbin committed 8b3f08b

Support MySQL backend.

Comments (0)

Files changed (6)

 iRedAPD-1.2.4:
+    * Support MySQL backend.
     * Add rc script for FreeBSD.
     * Ability to handle policy 'membersAndAllowedOnly'.
 

etc/iredapd.ini.sample

 #       INFO user@domain.ltd -> list@domain.ltd, DUNNO
 log_level       = info
 
+# Backend: ldap, mysql.
+backend = ldap
+
 [ldap]
+# For ldap backend only.
 # LDAP server setting.
 # Uri must starts with ldap:// or ldaps:// (TLS/SSL).
 #
 # Enabled plugins.
 #   - Plugin name is file name which placed under 'src/plugins/' directory.
 #   - Plugin names MUST be seperated by comma.
-plugins = maillist_access_policy
+plugins = ldap_maillist_access_policy
+
+[mysql]
+# For MySQL backend only.
+server      = 127.0.0.1
+db          = vmail
+user        = vmail
+password    = Psaf68wsuVctYSbj4PJzRqmFsE0rlQ
+alias_table = alias
+
+# Enabled plugins.
+#   - Plugin name is file name which placed under 'src/plugins/' directory.
+#   - Plugin names MUST be seperated by comma.
+plugins = sql_alias_access_policy
 import asyncore
 import asynchat
 import logging
-import ldap
 import daemon
 
 __version__ = "1.2.3"
                 self.map[key] = value
         elif len(self.map) != 0:
             try:
-                modeler = LDAPModeler()
+                if cfg.get('general', 'backend', 'ldap') == 'ldap':
+                    modeler = LDAPModeler()
+                else:
+                    modeler = MySQLModeler()
+
                 result = modeler.handle_data(self.map)
                 logging.debug("result replying: %s." % str(result))
                 if result != None:
         channel = apdChannel(conn, remoteaddr)
 
 
+class MySQLModeler:
+    def __init__(self):
+        import web
+        web.config.debug = False
+
+        self.db = web.database(
+            dbn='mysql',
+            host=cfg.get('mysql', 'server', 'localhost'),
+            db=cfg.get('mysql', 'db', 'vmail'),
+            user=cfg.get('mysql', 'user', 'vmail'),
+            pw=cfg.get('mysql', '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('mysql', 'plugins', '')
+            self.plugins = [v.strip() for v in self.plugins.split(',')]
+
+            # Get sender, recipient.
+            sender = map['sender']
+            sender_domain = sender.split('@', 1)[1]
+
+            recipient = map['recipient']
+            recipient_domain = recipient.split('@', 1)[1]
+
+            if len(self.plugins) > 0:
+                # Get alias account from alias table.
+                vars = dict(recipient=recipient, recipient_domain=recipient_domain, )
+                result = self.db.select(
+                    cfg.get('mysql', 'alias_table', 'alias'),
+                    vars, where='address = $recipient AND domain = $recipient_domain',
+                )
+
+                # Return if recipient account doesn't exist.
+                if len(result) != 1:
+                    logging.debug('No alias found: %s' % recipient)
+                    return ACTION_DEFAULT
+
+                #
+                # Import plugin modules.
+                #
+                self.modules = []
+
+                # Load plugin module.
+                for plugin in self.plugins:
+                    try:
+                        self.modules.append(__import__(plugin))
+                    except Exception, e:
+                        logging.debug('Error while importing plugin module (%s): %s' % (plugin, str(e)))
+
+                #
+                # Apply plugins.
+                #
+                self.action = ''
+                for module in self.modules:
+                    try:
+                        logging.debug('Apply plugin (%s).' % (module.__name__, ))
+                        pluginAction = module.restriction(
+                            dbConn=self.db,
+                            sqlRecord=result[0],
+                            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
+                    except Exception, e:
+                        logging.debug('Error while apply plugin (%s): %s' % (module, str(e)))
+
+            else:
+                # No plugins available.
+                return 'DUNNO'
+        else:
+            return ACTION_DEFER
+
+
+
 class LDAPModeler:
     def __init__(self):
+        import ldap
+
         # Read LDAP server settings.
         self.uri = cfg.get('ldap', 'uri', 'ldap://127.0.0.1:389')
         self.binddn = cfg.get('ldap', 'binddn')
 
                 # Return if recipient account doesn't exist.
                 if recipientDn is None or recipientLdif is None:
-                    logging.debug(str(e))
+                    logging.debug('Recipient DN or LDIF is none.')
                     return ACTION_DEFAULT
 
                 #
 def main():
     # Chroot in current directory.
     try:
-        os.chdir(os.path.dirname(__file__))
+        os.chdir(os.path.abspath(os.path.dirname(__file__)))
     except:
         pass
 

src/plugins/ldap_maillist_access_policy.py

+#!/usr/bin/env python
+# encoding: utf-8
+
+# Author: Zhang Huangbin <michaelbibby (at) gmail.com>
+
+# ----------------------------------------------------------------------------
+# This plugin is used for mail deliver restriction.
+# ----------------------------------------------------------------------------
+
+import sys
+
+ACTION_REJECT = 'REJECT Not Authorized'
+
+def __get_allowed_senders(ldapConn, ldapBaseDn, listDn, sender, recipient, policy,):
+    """return search_result_list_based_on_access_policy"""
+
+    basedn = ldapBaseDn
+    searchScope = 2     # Use SCOPE_BASE to improve performance.
+
+    # Set search base dn, scope, filter and attribute list based on access policy.
+    if policy == 'membersonly':
+        # Filter used to get domain members.
+        searchFilter = "(&(|(objectclass=mailUser)(objectClass=mailExternalUser))(accountStatus=active)(memberOfGroup=%s))" % (recipient, )
+        searchAttr = ['mail']
+    elif policy == 'allowedonly':
+        basedn = listDn
+        searchScope = 0     # Use SCOPE_BASE to improve performance.
+        # Filter used to get domain moderators.
+        searchFilter = "(&(objectclass=mailList)(mail=%s))" % (recipient, )
+        searchAttr = ['listAllowedUser']
+    else:
+        # Policy: membersAndAllowedOnly.
+        # Filter used to get both members and moderators.
+        searchFilter = "(|(&(|(objectClass=mailUser)(objectClass=mailExternalUser))(memberOfGroup=%s))(&(objectclass=mailList)(mail=%s)))" % (recipient, recipient, )
+        searchAttr = ['mail', 'listAllowedUser']
+
+    try:
+        result = ldapConn.search_s(basedn, searchScope, searchFilter, searchAttr)
+        userList = []
+        for obj in result:
+            for k in searchAttr:
+                if k in obj[1].keys():
+                    # Example of result data:
+                    # [('dn', {'listAllowedUser': ['user@domain.ltd']})]
+                    userList += obj[1][k]
+                else:
+                    pass
+        return userList
+
+    except Exception, e:
+        return []
+
+def restriction(ldapConn, ldapBaseDn, ldapRecipientDn, ldapRecipientLdif, smtpSessionData, **kargs):
+    # Return if recipient is not a mail list object.
+    if 'maillist' not in [ v.lower() for v in ldapRecipientLdif['objectClass']]:
+        return 'DUNNO'
+
+    sender = smtpSessionData['sender'].lower()
+    recipient = smtpSessionData['recipient'].lower()
+    policy = ldapRecipientLdif.get('accessPolicy', ['public'])[0].lower()
+
+    if policy == "public": return 'DUNNO'   # No restriction.
+    elif policy == "domain":
+        # Bypass all users under the same domain.
+        if sender.split('@')[1] == recipient.split('@')[1]: return 'DUNNO'
+        else: return ACTION_REJECT
+    else:
+        # Handle other access policies: membersOnly, allowedOnly, membersAndAllowedOnly.
+        allowedSenders = __get_allowed_senders(
+                ldapConn=ldapConn,
+                ldapBaseDn=ldapBaseDn,
+                listDn=ldapRecipientDn,
+                sender=sender,
+                recipient=recipient,
+                policy=policy,
+                )
+
+        if sender.lower() in [ v.lower() for v in allowedSenders ]:
+            return 'DUNNO'
+        else:
+            return ACTION_REJECT

src/plugins/maillist_access_policy.py

-#!/usr/bin/env python
-# encoding: utf-8
-
-# Author: Zhang Huangbin <michaelbibby (at) gmail.com>
-
-# ----------------------------------------------------------------------------
-# This plugin is used for mail deliver restriction.
-# ----------------------------------------------------------------------------
-
-import sys
-
-ACTION_REJECT = 'REJECT Not Authorized'
-
-def __get_allowed_senders(ldapConn, ldapBaseDn, listDn, sender, recipient, policy,):
-    """return search_result_list_based_on_access_policy"""
-
-    basedn = ldapBaseDn
-    searchScope = 2     # Use SCOPE_BASE to improve performance.
-
-    # Set search base dn, scope, filter and attribute list based on access policy.
-    if policy == 'membersonly':
-        # Filter used to get domain members.
-        searchFilter = "(&(|(objectclass=mailUser)(objectClass=mailExternalUser))(accountStatus=active)(memberOfGroup=%s))" % (recipient, )
-        searchAttr = ['mail']
-    elif policy == 'allowedonly':
-        basedn = listDn
-        searchScope = 0     # Use SCOPE_BASE to improve performance.
-        # Filter used to get domain moderators.
-        searchFilter = "(&(objectclass=mailList)(mail=%s))" % (recipient, )
-        searchAttr = ['listAllowedUser']
-    else:
-        # Policy: membersAndAllowedOnly.
-        # Filter used to get both members and moderators.
-        searchFilter = "(|(&(|(objectClass=mailUser)(objectClass=mailExternalUser))(memberOfGroup=%s))(&(objectclass=mailList)(mail=%s)))" % (recipient, recipient, )
-        searchAttr = ['mail', 'listAllowedUser']
-
-    try:
-        result = ldapConn.search_s(basedn, searchScope, searchFilter, searchAttr)
-        userList = []
-        for obj in result:
-            for k in searchAttr:
-                if k in obj[1].keys():
-                    # Example of result data:
-                    # [('dn', {'listAllowedUser': ['user@domain.ltd']})]
-                    userList += obj[1][k]
-                else:
-                    pass
-        return userList
-
-    except Exception, e:
-        return []
-
-def restriction(ldapConn, ldapBaseDn, ldapRecipientDn, ldapRecipientLdif, smtpSessionData, **kargs):
-    # Return if recipient is not a mail list object.
-    if 'maillist' not in [ v.lower() for v in ldapRecipientLdif['objectClass']]:
-        return 'DUNNO'
-
-    sender = smtpSessionData['sender'].lower()
-    recipient = smtpSessionData['recipient'].lower()
-    policy = ldapRecipientLdif.get('accessPolicy', ['public'])[0].lower()
-
-    if policy == "public": return 'DUNNO'   # No restriction.
-    elif policy == "domain":
-        # Bypass all users under the same domain.
-        if sender.split('@')[1] == recipient.split('@')[1]: return 'DUNNO'
-        else: return ACTION_REJECT
-    else:
-        # Handle other access policies: membersOnly, allowedOnly, membersAndAllowedOnly.
-        allowedSenders = __get_allowed_senders(
-                ldapConn=ldapConn,
-                ldapBaseDn=ldapBaseDn,
-                listDn=ldapRecipientDn,
-                sender=sender,
-                recipient=recipient,
-                policy=policy,
-                )
-
-        if sender.lower() in [ v.lower() for v in allowedSenders ]:
-            return 'DUNNO'
-        else:
-            return ACTION_REJECT

src/plugins/sql_alias_access_policy.py

+#!/usr/bin/env python
+# encoding: utf-8
+
+# Author: Zhang Huangbin <michaelbibby (at) gmail.com>
+# Date: 2010-03-12
+# Purpose: Apply access policy on sender while recipient is an alias.
+
+# -------- ALTER MYSQL TABLE BEFORE ENABLE THIS PLUGIN -----------
+#   mysql> USE vmail;
+#   mysql> ALTER TABLE alias ADD COLUMN accesspolicy VARCHAR(30) NOT NULL DEFAULT '';
+#   mysql> ALTER TABLE alias ADD COLUMN moderators TEXT NOT NULL DEFAULT '';
+# --------
+
+# Handled policies:
+#   - public:   Unrestricted
+#   - domain:   Only users under same domain are allowed.
+#   - membersOnly:  Only members are allowed.
+#   - moderatorsOnly:   Only moderators are allowed.
+#   - membersAndModeratorsOnly: Only members and moderators are allowed.
+
+import sys
+
+ACTION_REJECT = 'REJECT Not Authorized'
+
+# Policies. MUST be defined in lower case.
+POLICY_PUBLIC = 'public'
+POLICY_DOMAIN = 'domain'
+POLICY_MEMBERSONLY = 'membersonly'
+POLICY_MODERATORSONLY = 'moderatorsonly'
+POLICY_MEMBERSANDMODERATORSONLY = 'membersandmoderatorsonly'
+
+def restriction(dbConn, sqlRecord, smtpSessionData, **kargs):
+    policy = sqlRecord.get('accesspolicy', 'public').lower()
+    sender = smtpSessionData['sender'].lower()
+    recipient = smtpSessionData['recipient'].lower()
+    members = [str(v.lower()) for v in sqlRecord.get('goto', '').split(',')]
+    moderators = [str(v.lower()) for v in sqlRecord.get('moderators', '').split(',')]
+
+    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 sender.split('@')[1] == recipient.split('@')[1]:
+            return 'DUNNO'
+        else:
+            return ACTION_REJECT
+    elif policy == POLICY_MEMBERSONLY:
+        # Bypass all members.
+        if sender in members:
+            return 'DUNNO'
+        else:
+            return ACTION_REJECT
+    elif policy == POLICY_MEMBERSONLY:
+        # Bypass all moderators.
+        if sender in moderators:
+            return 'DUNNO'
+        else:
+            return ACTION_REJECT
+    elif policy == POLICY_MEMBERSANDMODERATORSONLY:
+        # Bypass both members and moderators.
+        if sender in members or sender in moderators:
+            return 'DUNNO'
+        else:
+            return ACTION_REJECT
+    else:
+        # Bypass all if policy is not defined in this plugin.
+        return 'DUNNO'