3 # Copyright (C) 2008-2012 W. Trevor King
4 # Copyright (C) 2012-2013 Wade Berrier
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 """LDAP address searches for Mutt.
21 Add :file:`mutt-ldap.py` to your ``PATH`` and add the following line
22 to your :file:`.muttrc`::
24 set query_command = "mutt-ldap.py '%s'"
26 Search for addresses with `^t`, optionally after typing part of the
27 name. Configure your connection by creating :file:`~/.mutt-ldap.rc`
28 contaning something like::
31 server = myserver.example.net
32 basedn = ou=people,dc=example,dc=net
34 See the `CONFIG` options for other available settings.
47 CONFIG = ConfigParser.SafeConfigParser()
48 CONFIG.add_section('connection')
49 CONFIG.set('connection', 'server', 'domaincontroller.yourdomain.com')
50 CONFIG.set('connection', 'port', '389') # set to 636 for default over SSL
51 CONFIG.set('connection', 'ssl', 'no')
52 CONFIG.set('connection', 'starttls', 'no')
53 CONFIG.set('connection', 'basedn', 'ou=x co.,dc=example,dc=net')
54 CONFIG.add_section('auth')
55 CONFIG.set('auth', 'user', '')
56 CONFIG.set('auth', 'password', '')
57 CONFIG.set('auth', 'gssapi', 'no')
58 CONFIG.add_section('query')
59 CONFIG.set('query', 'filter', '') # only match entries according to this filter
60 CONFIG.set('query', 'search_fields', 'cn displayName uid mail') # fields to wildcard search
61 CONFIG.add_section('results')
62 CONFIG.set('results', 'optional_column', '') # mutt can display one optional column
63 CONFIG.add_section('cache')
64 CONFIG.set('cache', 'enable', 'yes') # enable caching by default
65 CONFIG.set('cache', 'path', '~/.mutt-ldap.cache') # cache results here
66 #CONFIG.set('cache', 'longevity_days', '14') # TODO: cache results for 14 days by default
67 CONFIG.read(os.path.expanduser('~/.mutt-ldap.rc'))
71 if CONFIG.getboolean('connection', 'ssl'):
73 url = '{0}://{1}:{2}'.format(
75 CONFIG.get('connection', 'server'),
76 CONFIG.get('connection', 'port'))
77 connection = ldap.initialize(url)
78 if CONFIG.getboolean('connection', 'starttls') and protocol == 'ldap':
79 connection.start_tls_s()
80 if CONFIG.getboolean('auth', 'gssapi'):
81 sasl = ldap.sasl.gssapi()
82 connection.sasl_interactive_bind_s('', sasl)
85 CONFIG.get('auth', 'user'),
86 CONFIG.get('auth', 'password'),
90 def search(query, connection=None):
91 local_connection = False
94 local_connection = True
95 connection = connect()
99 filterstr = u'(|{0})'.format(
100 u' '.join([u'({0}=*{1}{2})'.format(field, query, post)
101 for field in CONFIG.get('query', 'search_fields').split()]))
102 query_filter = CONFIG.get('query', 'filter')
104 filterstr = u'(&({0}){1})'.format(query_filter, filterstr)
105 r = connection.search_s(
106 CONFIG.get('connection', 'basedn'),
108 filterstr.encode('utf-8'))
110 if local_connection and connection:
114 def format_columns(address, data):
116 yield data.get('displayName', data['cn'])[-1]
117 optional_column = CONFIG.get('results', 'optional_column')
118 if optional_column in data:
119 yield data[optional_column][-1]
121 def format_entry(entry):
124 for m in data['mail']:
125 # http://www.mutt.org/doc/manual/manual-4.html#ss4.5
126 # Describes the format mutt expects: address\tname
127 yield "\t".join(format_columns(m, data))
129 def cache_filename(query):
130 # TODO: is the query filename safe?
131 return os.path.expanduser(CONFIG.get('cache', 'path')) + os.sep + query
133 def settings_match(serialized_settings):
134 """Check to make sure the settings are the same for this cache"""
135 return pickle.dumps(CONFIG) == serialized_settings
137 def cache_lookup(query):
140 if CONFIG.get('cache', 'enable') == 'yes':
141 cache_file = cache_filename(query)
142 cache_dir = os.path.dirname(cache_file)
143 if not os.path.exists(cache_dir): os.mkdir(cache_dir)
145 # TODO: validate longevity setting
147 if os.path.exists(cache_file):
148 cache_info = pickle.loads(open(cache_file).read())
149 if settings_match(cache_info['settings']):
151 addresses = cache_info['addresses']
153 return hit, addresses
155 def cache_persist(query, addresses):
157 'settings': pickle.dumps(CONFIG),
158 'addresses': addresses
160 fd = open(cache_filename(query), 'w')
161 pickle.dump(cache_info, fd)
164 if __name__ == '__main__':
167 query = unicode(' '.join(sys.argv[1:]), 'utf-8')
169 (cache_hit, addresses) = cache_lookup(query)
172 entries = search(query)
173 addresses = list(itertools.chain(
174 *[format_entry(e) for e in sorted(entries)]))
176 # Cache results for next lookup
177 cache_persist(query, addresses)
179 print('{0} addresses found:'.format(len(addresses)))
180 print('\n'.join(addresses))