import textwrap
import copy
import shutil
+import shlex
import stat
# Third party modules
import ldap3
from ldap3 import ObjectDef, AttrDef, Reader, Writer
+from ldap3 import BASE, LEVEL, SUBTREE
from ldap3.core.exceptions import LDAPKeyError
from .mailaddress import MailAddress
-__version__ = '0.2.0'
+__version__ = '0.3.1'
LOG = logging.getLogger(__name__)
postfix_config_dir = os.sep + os.path.join('etc', 'postfix')
postfix_maps_dir = os.path.join(postfix_config_dir, 'maps')
- default_aliases_files = [
- os.path.join(postfix_maps_dir, 'aliases'),
- ]
-
default_virtaliases_files = [
os.path.join(postfix_maps_dir, 'virtual-aliases'),
]
default_origin = 'pixelpark.com'
- re_aliases_line = re.compile(r'^([^#\s:]+):', re.MULTILINE)
re_virtaliases_line = re.compile(r'^([^#\s:]+)\s', re.MULTILINE)
+ re_root_address = re.compile(r'^root(?:@.*)?\s*$', re.IGNORECASE)
+
+ open_args = {}
+ if six.PY3:
+ open_args = {
+ 'encoding': 'utf-8',
+ 'errors': 'surrogateescape',
+ }
# -------------------------------------------------------------------------
def __init__(self, appname=None, version=__version__):
self.barracuda_base_dn = self.default_barracuda_base_dn
- self.aliases_files = copy.copy(self.default_aliases_files)
self.virtaliases_files = copy.copy(self.default_virtaliases_files)
self.origin = self.default_origin
self._show_simulate_opt = True
description = textwrap.dedent('''\
- Synchronization of existing aliases and virtual aliases
+ Synchronization of existing virtual aliases
with alias definitions in LDAP for Barracuda.
''').strip()
self.initialized = True
+ # -------------------------------------------------------------------------
+ def as_dict(self, short=True):
+ """
+ Transforms the elements of the object into a dict
+
+ @param short: don't include local properties in resulting dict.
+ @type short: bool
+
+ @return: structure as dict
+ @rtype: dict
+ """
+
+ res = super(PpBarracudaSyncApp, self).as_dict(short=short)
+ res['default_barracuda_base_dn'] = self.default_barracuda_base_dn
+ res['postfix_config_dir'] = self.postfix_config_dir
+ res['postfix_maps_dir'] = self.postfix_maps_dir
+ res['default_virtaliases_files'] = self.default_virtaliases_files
+ res['default_origin'] = self.default_origin
+ res['open_args'] = self.open_args
+
+ return res
+
+ # -------------------------------------------------------------------------
+ def init_arg_parser(self):
+ """
+ Method to initiate the argument parser.
+
+ This method should be explicitely called by all init_arg_parser()
+ methods in descendant classes.
+ """
+
+ self.arg_parser.add_argument(
+ '-P', '--postfix-dir',
+ metavar="DIR", dest='postfix_dir',
+ help="Configuration directory for Postfix (default: {!r}).".format(
+ self.postfix_config_dir)
+ )
+
+ super(PpBarracudaSyncApp, self).init_arg_parser()
+
+ # -------------------------------------------------------------------------
+ def perform_config(self):
+
+ super(PpBarracudaSyncApp, self).perform_config()
+
+ aliases_files = None
+ virtaliases_files = None
+
+ for section_name in self.cfg.keys():
+
+ if self.verbose > 2:
+ LOG.debug("Checking config section {!r} ...".format(section_name))
+
+ if section_name.lower() not in ('barracuda-sync', 'barracuda_sync', 'barracudasync') :
+ continue
+
+ section = self.cfg[section_name]
+ if self.verbose > 2:
+ LOG.debug("Evaluating config section {n!r}:\n{s}".format(
+ n=section_name, s=pp(section)))
+
+ if 'postfix_dir' in section:
+ self._init_postfix_dir(section['postfix_dir'])
+
+ if 'virtaliases_files' in section:
+ v = section['virtaliases_files']
+ files = shlex.split(v)
+ if files:
+ if virtaliases_files is None:
+ virtaliases_files = []
+ for f in files:
+ if f not in virtaliases_files:
+ virtaliases_files.append(f)
+
+ if 'base_dn' in section:
+ v = section['base_dn'].strip()
+ if v:
+ self.barracuda_base_dn = v
+
+ if 'origin' in section:
+ v = section['origin'].strip()
+ if v:
+ self.origin = v
+
+ if hasattr(self.args, 'postfix_dir') and self.args.postfix_dir:
+ self._init_postfix_dir(self.args.postfix_dir)
+
+ self._init_virtaliases_files(virtaliases_files)
+
+ # -------------------------------------------------------------------------
+ def _init_virtaliases_files(self, virtaliases_files):
+
+ self.virtaliases_files = copy.copy(self.default_virtaliases_files)
+ if virtaliases_files is None:
+ return
+
+ self.virtaliases_files = []
+ for afile in virtaliases_files:
+ if not os.path.isabs(afile):
+ afile = os.path.join(self.postfix_config_dir, afile)
+ afile = os.path.normpath(afile)
+ if afile not in self.virtaliases_files:
+ self.virtaliases_files.append(afile)
+
+ # -------------------------------------------------------------------------
+ def _init_postfix_dir(self, value):
+
+ if os.path.isdir(value):
+ d = os.path.abspath(value)
+ self.postfix_config_dir = d
+ self.postfix_maps_dir = os.path.join(d, 'maps')
+ self.default_aliases_files = [
+ os.path.join(self.postfix_maps_dir, 'aliases'),
+ ]
+ self.default_virtaliases_files = [
+ os.path.join(self.postfix_maps_dir, 'virtual-aliases'),
+ ]
+
# -------------------------------------------------------------------------
def pre_run(self):
"""
LOG.info("Starting ...")
+ self.read_virtaliases_files()
+ self.existing_aliases.sort(key=str.lower)
+ LOG.info("Found {} existing aliases.".format(len(self.existing_aliases)))
+ if self.verbose > 1:
+ LOG.debug("Existing aliases:\n{}".format(pp(self.existing_aliases)))
+
+ self.read_ldap_aliases()
+
+ self.eval_diffs()
+
+ # -------------------------------------------------------------------------
+ def read_virtaliases_files(self):
+
+ LOG.info("Reading all virtual aliases files ...")
+ for afile in self.virtaliases_files:
+ if self.verbose > 1:
+ LOG.debug("Checking for virtaliases file {!r} ...".format(afile))
+ if not os.path.isfile(afile):
+ continue
+ content = ''
+ LOG.debug("Reading virtaliases file {!r} ...".format(afile))
+ with open(afile, 'r', **self.open_args) as fh:
+ content = fh.read()
+ aliases = self.re_virtaliases_line.findall(content)
+ for alias in aliases:
+ if self.re_root_address.match(alias):
+ continue
+ if alias not in self.existing_aliases:
+ if self.verbose > 2:
+ LOG.debug("Registring existing alias {!r}.".format(alias))
+ self.existing_aliases.append(alias)
+
+ # -------------------------------------------------------------------------
+ def read_ldap_aliases(self):
+
+ LOG.info("Reading all aliases from LDAP ...")
+
+ alias = ObjectDef(['mailRecipient'])
+ alias += ['cn', 'mail']
+
+ query_filter = '(&(objectclass=mailRecipient)(mail=*))'
+
+ entries = self.ldap_search_subtree(alias, query_filter, base=self.barracuda_base_dn)
+
+ for entry in entries:
+ dn = entry.entry_dn
+ cn = entry['cn'][0]
+ mail = entry['mail'][0]
+ if self.verbose > 3:
+ LOG.debug("Found LDAP alias, DN: {dn!r}, CN: {cn!r}, Mail: {m!r}.".format(
+ dn=dn, cn=cn, m=mail))
+
+ if not cn:
+ continue
+ if cn not in self.ldap_aliases:
+ if self.verbose > 2:
+ LOG.debug("Registring LDAP alias {!r}.".format(cn))
+ self.ldap_aliases.append(cn)
+
+ self.ldap_aliases.sort(key=str.lower)
+ LOG.info("Found {} LDAP aliases.".format(len(self.ldap_aliases)))
+ if self.verbose > 1:
+ LOG.debug("LDAP aliases:\n{}".format(pp(self.ldap_aliases)))
+
+ # -------------------------------------------------------------------------
+ def eval_diffs(self):
+
+ LOG.info("Evaluating differences ...")
+
+ self.aliases_to_create = []
+ self.aliases_to_remove = []
+
+ for alias in self.existing_aliases:
+ if alias not in self.ldap_aliases and alias not in self.aliases_to_create:
+ self.aliases_to_create.append(alias)
+
+ for alias in self.ldap_aliases:
+ if alias not in self.existing_aliases and alias not in self.aliases_to_remove:
+ self.aliases_to_remove.append(alias)
+
+ LOG.info("Aliases to create in LDAP:\n{}".format(pp(self.aliases_to_create)))
+ LOG.info("Aliases to remove from LDAP:\n{}".format(pp(self.aliases_to_remove)))
+
# =============================================================================
if __name__ == "__main__":