Merge remote-tracking branch 'wberrier/master'
[mutt-ldap.git] / mutt_ldap.py
index f3bb5bae659370b3ff3706072da3045dfed24d47..c63c04e5e73abde5507ebc3b01c8580a5a4561fd 100755 (executable)
 
 "LDAP address searches for Mutt"
 
+import codecs as _codecs
 import ConfigParser as _configparser
 import hashlib as _hashlib
 import json as _json
+import locale as _locale
 import logging as _logging
 import os.path as _os_path
+import os as _os
 import pickle as _pickle
+import sys as _sys
 import time as _time
 
 import ldap as _ldap
 import ldap.sasl as _ldap_sasl
 
+_xdg_import_error = None
+try:
+    import xdg.BaseDirectory as _xdg_basedirectory
+except ImportError as _xdg_import_error:
+    _xdg_basedirectory = None
+
 
 __version__ = '0.1'
 
@@ -38,7 +48,89 @@ LOG = _logging.getLogger('mutt-ldap')
 LOG.addHandler(_logging.StreamHandler())
 LOG.setLevel(_logging.ERROR)
 
-CONFIG = _configparser.SafeConfigParser()
+
+class Config (_configparser.SafeConfigParser):
+    def load(self):
+        config_paths = self._get_config_paths()
+        LOG.info(u'load configuration from {0}'.format(config_paths))
+        read_config_paths = self.read(config_paths)
+        self._setup_defaults()
+        LOG.info(u'loaded configuration from {0}'.format(read_config_paths))
+
+    def get_connection_class(self):
+        if self.getboolean('cache', 'enable'):
+            return CachedLDAPConnection
+        else:
+            return LDAPConnection
+
+    def _setup_defaults(self):
+        "Setup dynamic default values"
+        self._setup_encoding_defaults()
+        self._setup_cache_defaults()
+
+    def _setup_encoding_defaults(self):
+        default_encoding = _locale.getpreferredencoding(do_setlocale=True)
+        for key in ['output-encoding', 'argv-encoding']:
+            self.set(
+                'system', key,
+                self.get('system', key, raw=True) or default_encoding)
+
+        # HACK: convert sys.std{out,err} to Unicode (not needed in Python 3)
+        output_encoding = self.get('system', 'output-encoding')
+        _sys.stdout = _codecs.getwriter(output_encoding)(_sys.stdout)
+        _sys.stderr = _codecs.getwriter(output_encoding)(_sys.stderr)
+
+        # HACK: convert sys.argv to Unicode (not needed in Python 3)
+        argv_encoding = self.get('system', 'argv-encoding')
+        _sys.argv = [unicode(arg, argv_encoding) for arg in _sys.argv]
+
+    def _setup_cache_defaults(self):
+        if not self.get('cache', 'path'):
+            self.set('cache', 'path', self._get_cache_path())
+        if not self.get('cache', 'fields'):
+            # setup a reasonable default
+            fields = ['mail', 'cn', 'displayName']  # used by format_entry()
+            optional_column = self.get('results', 'optional-column')
+            if optional_column:
+                fields.append(optional_column)
+            self.set('cache', 'fields', ' '.join(fields))
+
+    def _get_config_paths(self):
+        "Get configuration file paths"
+        if _xdg_basedirectory:
+            paths = list(reversed(list(
+                        _xdg_basedirectory.load_config_paths(''))))
+            if not paths:  # setup something for a useful log message
+                paths.append(_xdg_basedirectory.save_config_path(''))
+        else:
+            self._log_xdg_import_error()
+            paths = [_os_path.expanduser(_os_path.join('~', '.config'))]
+        return [_os_path.join(path, 'mutt-ldap.cfg') for path in paths]
+
+    def _get_cache_path(self):
+        "Get the cache file path"
+
+        # Some versions of pyxdg don't have save_cache_path (0.20 and older)
+        # See: https://bugs.freedesktop.org/show_bug.cgi?id=26458
+        if _xdg_basedirectory and 'save_cache_path' in dir(_xdg_basedirectory):
+            path = _xdg_basedirectory.save_cache_path('')
+        else:
+            self._log_xdg_import_error()
+            path = _os_path.expanduser(_os_path.join('~', '.cache'))
+            if not _os_path.isdir(path):
+                _os.makedirs(path)
+        return _os_path.join(path, 'mutt-ldap.json')
+
+    def _log_xdg_import_error(self):
+        global _xdg_import_error
+        if _xdg_import_error:
+            LOG.warning(u'could not import xdg.BaseDirectory '
+                u'or lacking necessary support')
+            LOG.warning(_xdg_import_error)
+            _xdg_import_error = None
+
+
+CONFIG = Config()
 CONFIG.add_section('connection')
 CONFIG.set('connection', 'server', 'domaincontroller.yourdomain.com')
 CONFIG.set('connection', 'port', '389')  # set to 636 for default over SSL
@@ -56,14 +148,14 @@ CONFIG.add_section('results')
 CONFIG.set('results', 'optional-column', '') # mutt can display one optional column
 CONFIG.add_section('cache')
 CONFIG.set('cache', 'enable', 'yes') # enable caching by default
-CONFIG.set('cache', 'path', '~/.mutt-ldap.cache') # cache results here
+CONFIG.set('cache', 'path', '') # cache results here, defaults to XDG
 CONFIG.set('cache', 'fields', '')  # fields to cache (if empty, setup in the main block)
-CONFIG.set('cache', 'longevity-days', '14') # TODO: cache results for 14 days by default
+CONFIG.set('cache', 'longevity-days', '14') # Days before cache entries are invalidated
 CONFIG.add_section('system')
+# HACK: Python 2.x support, see http://bugs.python.org/issue13329#msg147475
+CONFIG.set('system', 'output-encoding', '')  # match .muttrc's $charset
 # HACK: Python 2.x support, see http://bugs.python.org/issue2128
-CONFIG.set('system', 'argv-encoding', 'utf-8')
-
-CONFIG.read(_os_path.expanduser('~/.mutt-ldap.rc'))
+CONFIG.set('system', 'argv-encoding', '')
 
 
 class LDAPConnection (object):
@@ -264,30 +356,15 @@ def format_entry(entry):
 
 
 if __name__ == '__main__':
-    import sys
-
-    # HACK: convert sys.argv to Unicode (not needed in Python 3)
-    argv_encoding = CONFIG.get('system', 'argv-encoding')
-    sys.argv = [unicode(arg, argv_encoding) for arg in sys.argv]
-
-    if len(sys.argv) < 2:
-        sys.stderr.write(u'{0}: no search string given\n'.format(sys.argv[0]))
-        sys.exit(1)
+    CONFIG.load()
 
-    query = u' '.join(sys.argv[1:])
+    if len(_sys.argv) < 2:
+        LOG.error(u'{0}: no search string given'.format(_sys.argv[0]))
+        _sys.exit(1)
 
-    if CONFIG.getboolean('cache', 'enable'):
-        connection_class = CachedLDAPConnection
-        if not CONFIG.get('cache', 'fields'):
-            # setup a reasonable default
-            fields = ['mail', 'cn', 'displayName']  # used by format_entry()
-            optional_column = CONFIG.get('results', 'optional-column')
-            if optional_column:
-                fields.append(optional_column)
-            CONFIG.set('cache', 'fields', ' '.join(fields))
-    else:
-        connection_class = LDAPConnection
+    query = u' '.join(_sys.argv[1:])
 
+    connection_class = CONFIG.get_connection_class()
     addresses = []
     with connection_class() as connection:
         entries = connection.search(query=query)