make search fields configurable
[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.set('query', 'search_fields', 'cn displayName uid mail') # fields to wildcard search
60 CONFIG.add_section('results')
61 CONFIG.set('results', 'optional_column', '') # mutt can display one optional column
62 CONFIG.read(os.path.expanduser('~/.mutt-ldap.rc'))
63
64 def connect():
65     protocol = 'ldap'
66     if CONFIG.getboolean('connection', 'ssl'):
67         protocol = 'ldaps'
68     url = '{0}://{1}:{2}'.format(
69         protocol,
70         CONFIG.get('connection', 'server'),
71         CONFIG.get('connection', 'port'))
72     connection = ldap.initialize(url)
73     if CONFIG.getboolean('connection', 'starttls') and protocol == 'ldap':
74         connection.start_tls_s()
75     if CONFIG.getboolean('auth', 'gssapi'):
76         sasl = ldap.sasl.gssapi()
77         connection.sasl_interactive_bind_s('', sasl)
78     else:
79         connection.bind(
80             CONFIG.get('auth', 'user'),
81             CONFIG.get('auth', 'password'),
82             ldap.AUTH_SIMPLE)
83     return connection
84
85 def search(query, connection=None):
86     local_connection = False
87     try:
88         if not connection:
89             local_connection = True
90             connection = connect()
91         post = ''
92         if query:
93             post = '*'
94         filterstr = u'(|{0})'.format(
95             u' '.join([u'({0}=*{1}{2})'.format(field, query, post)
96                        for field in CONFIG.get('query', 'search_fields').split()]))
97         query_filter = CONFIG.get('query', 'filter')
98         if query_filter:
99             filterstr = u'(&({0}){1})'.format(query_filter, filterstr)
100         r = connection.search_s(
101             CONFIG.get('connection', 'basedn'),
102             ldap.SCOPE_SUBTREE,
103             filterstr.encode('utf-8'))
104     finally:
105         if local_connection and connection:
106             connection.unbind()
107     return r
108
109 def format_columns(address, data):
110     yield address
111     yield data.get('displayName', data['cn'])[-1]
112     optional_column = CONFIG.get('results', 'optional_column')
113     if optional_column in data:
114         yield data[optional_column][-1]
115
116 def format_entry(entry):
117     cn,data = entry
118     if 'mail' in data:
119         for m in data['mail']:
120             # http://www.mutt.org/doc/manual/manual-4.html#ss4.5
121             # Describes the format mutt expects: address\tname
122             yield "\t".join(format_columns(m, data))
123
124
125 if __name__ == '__main__':
126     import sys
127
128     query = unicode(' '.join(sys.argv[1:]), 'utf-8')
129     entries = search(query)
130     addresses = list(itertools.chain(
131             *[format_entry(e) for e in sorted(entries)]))
132     print('{0} addresses found:'.format(len(addresses)))
133     print('\n'.join(addresses))