Add query filter section (in order to pare down results)
[mutt-ldap.git] / mutt-ldap.py
1 #!/usr/bin/env python2
2 #
3 # Copyright (C) 2008-2012  W. Trevor King
4 # Copyright (C) 2012-2013  Wade Berrier
5 #
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.
10 #
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.
15 #
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/>.
18
19 """LDAP address searches for Mutt.
20
21 Add :file:`mutt-ldap.py` to your ``PATH`` and add the following line
22 to your :file:`.muttrc`::
23
24   set query_command = "mutt-ldap.py '%s'"
25
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::
29
30   [connection]
31   server = myserver.example.net
32   basedn = ou=people,dc=example,dc=net
33
34 See the `CONFIG` options for other available settings.
35 """
36
37 import email.utils
38 import itertools
39 import os.path
40 import ConfigParser
41
42 import ldap
43 import ldap.sasl
44
45
46 CONFIG = ConfigParser.SafeConfigParser()
47 CONFIG.add_section('connection')
48 CONFIG.set('connection', 'server', 'domaincontroller.yourdomain.com')
49 CONFIG.set('connection', 'port', '389')  # set to 636 for default over SSL
50 CONFIG.set('connection', 'ssl', 'no')
51 CONFIG.set('connection', 'starttls', 'no')
52 CONFIG.set('connection', 'basedn', 'ou=x co.,dc=example,dc=net')
53 CONFIG.add_section('auth')
54 CONFIG.set('auth', 'user', '')
55 CONFIG.set('auth', 'password', '')
56 CONFIG.set('auth', 'gssapi', 'no')
57 CONFIG.add_section('query')
58 CONFIG.set('query', 'filter', '') # only match entries according to this filter
59 CONFIG.read(os.path.expanduser('~/.mutt-ldap.rc'))
60
61 def connect():
62     protocol = 'ldap'
63     if CONFIG.getboolean('connection', 'ssl'):
64         protocol = 'ldaps'
65     url = '{}://{}:{}'.format(
66         protocol,
67         CONFIG.get('connection', 'server'),
68         CONFIG.get('connection', 'port'))
69     connection = ldap.initialize(url)
70     if CONFIG.getboolean('connection', 'starttls') and protocol == 'ldap':
71         connection.start_tls_s()
72     if CONFIG.getboolean('auth', 'gssapi'):
73         sasl = ldap.sasl.gssapi()
74         connection.sasl_interactive_bind_s('', sasl)
75     else:
76         connection.bind(
77             CONFIG.get('auth', 'user'),
78             CONFIG.get('auth', 'password'),
79             ldap.AUTH_SIMPLE)
80     return connection
81
82 def search(query, connection=None):
83     local_connection = False
84     try:
85         if not connection:
86             local_connection = True
87             connection = connect()
88         post = ''
89         if query:
90             post = '*'
91         filterstr = u'(|{})'.format(
92             u' '.join([u'({}=*{}{})'.format(field, query, post)
93                        for field in ['cn', 'displayName', 'uid', 'mail']]))
94         query_filter = CONFIG.get('query', 'filter')
95         if query_filter:
96             filterstr = u'(&({}){})'.format(query_filter, filterstr)
97         r = connection.search_s(
98             CONFIG.get('connection', 'basedn'),
99             ldap.SCOPE_SUBTREE,
100             filterstr.encode('utf-8'))
101     finally:
102         if local_connection and connection:
103             connection.unbind()
104     return r
105
106 def format_entry(entry):
107     cn,data = entry
108     if 'mail' in data:
109         name = data.get('displayName', data['cn'])[-1]
110         for m in data['mail']:
111             # http://www.mutt.org/doc/manual/manual-4.html#ss4.5
112             # Describes the format mutt expects: address\tname
113             yield "\t".join([m, name])
114
115
116 if __name__ == '__main__':
117     import sys
118
119     query = unicode(' '.join(sys.argv[1:]), 'utf-8')
120     entries = search(query)
121     addresses = list(itertools.chain(
122             *[format_entry(e) for e in sorted(entries)]))
123     print('{} addresses found:'.format(len(addresses)))
124     print('\n'.join(addresses))