3 # Copyright (C) 2008-2013 W. Trevor King
4 # Copyright (C) 2012-2013 Wade Berrier
5 # Copyright (C) 2012 Niels de Vos
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """LDAP address searches for Mutt.
22 Add :file:`mutt-ldap.py` to your ``PATH`` and add the following line
23 to your :file:`.muttrc`::
25 set query_command = "mutt-ldap.py '%s'"
27 Search for addresses with `^t`, optionally after typing part of the
28 name. Configure your connection by creating :file:`~/.mutt-ldap.rc`
29 contaning something like::
32 server = myserver.example.net
33 basedn = ou=people,dc=example,dc=net
35 See the `CONFIG` options for other available settings.
48 CONFIG = ConfigParser.SafeConfigParser()
49 CONFIG.add_section('connection')
50 CONFIG.set('connection', 'server', 'domaincontroller.yourdomain.com')
51 CONFIG.set('connection', 'port', '389') # set to 636 for default over SSL
52 CONFIG.set('connection', 'ssl', 'no')
53 CONFIG.set('connection', 'starttls', 'no')
54 CONFIG.set('connection', 'basedn', 'ou=x co.,dc=example,dc=net')
55 CONFIG.add_section('auth')
56 CONFIG.set('auth', 'user', '')
57 CONFIG.set('auth', 'password', '')
58 CONFIG.set('auth', 'gssapi', 'no')
59 CONFIG.add_section('query')
60 CONFIG.set('query', 'filter', '') # only match entries according to this filter
61 CONFIG.set('query', 'search_fields', 'cn displayName uid mail') # fields to wildcard search
62 CONFIG.add_section('results')
63 CONFIG.set('results', 'optional_column', '') # mutt can display one optional column
64 CONFIG.add_section('cache')
65 CONFIG.set('cache', 'enable', 'yes') # enable caching by default
66 CONFIG.set('cache', 'path', '~/.mutt-ldap.cache') # cache results here
67 #CONFIG.set('cache', 'longevity_days', '14') # TODO: cache results for 14 days by default
68 CONFIG.add_section('system')
69 # HACK: Python 2.x support, see http://bugs.python.org/issue2128
70 CONFIG.set('system', 'argv-encoding', 'utf-8')
72 CONFIG.read(os.path.expanduser('~/.mutt-ldap.rc'))
77 if CONFIG.getboolean('connection', 'ssl'):
79 url = '{0}://{1}:{2}'.format(
81 CONFIG.get('connection', 'server'),
82 CONFIG.get('connection', 'port'))
83 connection = ldap.initialize(url)
84 if CONFIG.getboolean('connection', 'starttls') and protocol == 'ldap':
85 connection.start_tls_s()
86 if CONFIG.getboolean('auth', 'gssapi'):
87 sasl = ldap.sasl.gssapi()
88 connection.sasl_interactive_bind_s('', sasl)
91 CONFIG.get('auth', 'user'),
92 CONFIG.get('auth', 'password'),
96 def search(query, connection):
100 filterstr = u'(|{0})'.format(
101 u' '.join([u'({0}=*{1}{2})'.format(field, query, post)
102 for field in CONFIG.get('query', 'search_fields').split()]))
103 query_filter = CONFIG.get('query', 'filter')
105 filterstr = u'(&({0}){1})'.format(query_filter, filterstr)
106 msg_id = connection.search(
107 CONFIG.get('connection', 'basedn'),
109 filterstr.encode('utf-8'))
111 while res_type != ldap.RES_SEARCH_RESULT:
113 res_type, res_data = connection.result(
114 msg_id, all=False, timeout=0)
115 except ldap.ADMINLIMIT_EXCEEDED:
116 #print "Partial results"
119 # use `yield from res_data` in Python >= 3.3, see PEP 380
120 for entry in res_data:
123 def format_columns(address, data):
124 yield unicode(address, 'utf-8')
125 yield unicode(data.get('displayName', data['cn'])[-1], 'utf-8')
126 optional_column = CONFIG.get('results', 'optional_column')
127 if optional_column in data:
128 yield unicode(data[optional_column][-1], 'utf-8')
130 def format_entry(entry):
133 for m in data['mail']:
134 # http://www.mutt.org/doc/manual/manual-4.html#ss4.5
135 # Describes the format mutt expects: address\tname
136 yield u'\t'.join(format_columns(m, data))
138 def cache_filename(query):
139 # TODO: is the query filename safe?
140 return os.path.expanduser(CONFIG.get('cache', 'path')) + os.sep + query
142 def settings_match(serialized_settings):
143 """Check to make sure the settings are the same for this cache"""
144 return pickle.dumps(CONFIG) == serialized_settings
146 def cache_lookup(query):
149 if CONFIG.get('cache', 'enable') == 'yes':
150 cache_file = cache_filename(query)
151 cache_dir = os.path.dirname(cache_file)
152 if not os.path.exists(cache_dir): os.mkdir(cache_dir)
154 # TODO: validate longevity setting
156 if os.path.exists(cache_file):
157 cache_info = pickle.loads(open(cache_file).read())
158 if settings_match(cache_info['settings']):
160 addresses = cache_info['addresses']
162 return hit, addresses
164 def cache_persist(query, addresses):
166 'settings': pickle.dumps(CONFIG),
167 'addresses': addresses
169 fd = open(cache_filename(query), 'w')
170 pickle.dump(cache_info, fd)
173 if __name__ == '__main__':
176 # HACK: convert sys.argv to Unicode (not needed in Python 3)
177 argv_encoding = CONFIG.get('system', 'argv-encoding')
178 sys.argv = [unicode(arg, argv_encoding) for arg in sys.argv]
180 if len(sys.argv) < 2:
181 sys.stderr.write(u'{0}: no search string given\n'.format(sys.argv[0]))
184 query = u' '.join(sys.argv[1:])
186 (cache_hit, addresses) = cache_lookup(query)
191 connection = connect()
192 entries = search(query=query, connection=connection)
193 for entry in entries:
194 addresses.extend(format_entry(entry))
195 cache_persist(query, addresses)
200 print(u'{0} addresses found:'.format(len(addresses)))
201 print(u'\n'.join(addresses))