Add starttls support to mutt-ldap.py and ldap-jpeg.py.
[blog.git] / posts / LDAP / abook-ldif-cleanup.py
1 #!/usr/bin/env python
2 #
3 # Copy...
4
5 """Cleanup the LDIF output from abook_ using `python-ldap`_.
6
7 .. _abook: http://abook.sourceforge.net/
8 .. _python-ldap: http://www.python-ldap.org/
9 """
10
11 import re
12 import StringIO
13
14 import ldif
15
16
17 def cleanup(text, basedn):
18     # pre-parser formatting
19     text = remove_trailing_mail(text)
20     text = remove_cn_commas(text)
21
22     records = ldif.ParseLDIF(StringIO.StringIO(text))
23
24     # post-parser formatting
25     records = remove_empty_mail(records)
26     records = remove_top_objectclass(records)
27     records = add_inetorgperson_objectclass(records)
28     records = add_base_dn(records, basedn)
29     records = add_names(records)
30     records = standardize_phone_numbers(records)
31     records = standardize_country_code(records)
32     records = rename_locality(records)
33     records = rename_cellphone(records)
34     records = rename_xmozillaanyphone(records)
35     records = rename_xmozillanickname(records)
36     records = rename_homeurl(records)
37     records = set_postaladdress(records)
38
39     # convert back to a string
40     s = StringIO.StringIO()
41     writer = ldif.LDIFWriter(s)
42     for dn,record in records:
43         writer.unparse(dn, record)
44     return 'version: 1\n\n%s' % s.getvalue()
45
46 def remove_trailing_mail(text):
47     """
48     >>> print(remove_trailing_mail('\\n'.join([
49     ...     'version: 1',
50     ...     'dn: cn=John Doe,mail=',
51     ...     'cn: John Doe',
52     ...     '',
53     ...     ])))
54     version: 1
55     dn: cn=John Doe,mail=x@y.com
56     cn: John Doe
57     <BLANKLINE>
58     """
59     return re.sub(',mail=$', ',mail=x@y.com', text, flags=re.MULTILINE)
60
61 def _sub_cn_commas(match):
62     cn = match.group(1).replace(',', '_')
63     return 'cn=%s,mail=' % cn
64
65 def remove_cn_commas(text):
66     """
67     >>> print(remove_cn_commas('\\n'.join([
68     ...     'version: 1',
69     ...     'dn: cn=John, Jane, and Jim Doe,mail=x@y.com',
70     ...     'cn: John, Jane, and Jim Doe',
71     ...     '',
72     ...     ])))
73     version: 1
74     dn: cn=John_ Jane_ and Jim Doe,mail=x@y.com
75     cn: John, Jane, and Jim Doe
76     <BLANKLINE>
77     """
78     return re.sub('cn=(.*),mail=', _sub_cn_commas, text)
79
80 def remove_empty_mail(records):
81     for dn,record in records:
82         if 'mail' in record and record['mail'] == ['']:
83             record.pop('mail')
84     return records
85
86 def remove_top_objectclass(records):
87     for dn,record in records:
88         if 'top' in record['objectclass']:
89             record['objectclass'].remove('top')
90     return records
91
92 def add_inetorgperson_objectclass(records):
93     for dn,record in records:
94         record['objectclass'].extend(
95             ['organizationalPerson', 'inetOrgPerson', 'extensibleObject'])
96         # extensibleObject required for countryName
97     return records
98
99 def add_base_dn(records, basedn):
100     regexp = re.compile(',mail=.*')
101     subst = ', ' + basedn
102     for i,(dn,record) in enumerate(records):
103         new_dn = regexp.sub(subst, dn)
104         records[i] = (new_dn, record)
105     return records
106
107 def _set_key(record, key, value, override=True):
108     """Case-agnostic value setter.
109
110     >>> record = {'aB': 'old'}
111     >>> _set_key(record, 'AB', 'new')
112     >>> print record
113     """
114     key = key.lower()
115     keys = [k for k in record.keys() if k.lower() == key.lower()]
116     if keys:
117         k = keys[0]
118     else:
119         k = key
120     if override or k not in record:
121         record[k] = value
122
123 def add_names(records):
124     """
125     Surname and givenName are defined in `RFC 4519`_.
126
127     .. _RFC 4512: http://tools.ietf.org/html/rfc4519
128     """
129     for dn,record in records:
130         cn = record['cn']
131         gn,sn = cn[0].rsplit(' ', 1)
132         _set_key(record, 'sn', [sn], override=False)
133         _set_key(record, 'givenName', [gn], override=False)
134     return records
135
136 def standardize_phone_numbers(records):
137     """Standardize phone numbers to match `E.123`_ international notation
138
139     Assumes numbers not starting with a '+' live in the USA.
140
141     >>> import pprint
142     >>> records = [
143     ...     ('cn=John', {'homephone': '123-456-7890'},
144     ...     ('cn=Jane', {TODO})]
145     >>> pprint.pprint(standardize_phone_numbers(records))
146
147     .. _E.123: http://en.wikipedia.org/wiki/E.123
148     """
149     # TODO
150     return records
151
152 def standardize_country_code(records):
153     # TODO
154     # ISO3166
155     # http://tools.ietf.org/html/rfc4519
156     # http://tools.ietf.org/html/rfc4517
157     #USA      US
158     #Canada   CA
159     #Bermuda  BM
160     #Bahamas  BS
161     #Netherlands NL
162     table = {
163         'USA': 'US',
164         'Canada': 'CA',
165         'Bermuda': 'BM',
166         'Bahamas': 'BS',
167         'Netherlands': 'NL',
168         }
169     for dn,record in records:
170         if 'countryname' in record:
171             record['countryname'] = [
172                 table.get(c, c) for c in record['countryname']]
173     return records
174
175 def rename_locality(records):
176     # locality -> l (localityName)
177     for dn,record in records:
178         if 'locality' in record:
179             record['localityname'] = record.pop('locality')
180     return records
181
182 def rename_cellphone(records):
183     # cellphone -> mobile
184     for dn,record in records:
185         if 'cellphone' in record:
186             record['mobile'] = record.pop('cellphone')
187     return records
188
189 def rename_xmozillaanyphone(records):
190     # xmozillaanyphone -> telephonenumber
191     for dn,record in records:
192         if 'xmozillaanyphone' in record:
193             record['telephonenumber'] = record.pop('xmozillaanyphone')
194     return records
195
196 def rename_xmozillanickname(records):
197     # xmozillanickname -> displayname
198     for dn,record in records:
199         if 'xmozillanickname' in record:
200             record['displayname'] = record.pop('xmozillanickname')
201     return records
202
203 def rename_homeurl(records):
204     # homeurl -> labeledURI
205     for dn,record in records:
206         if 'homeurl' in record:
207             record['labeleduri'] = [
208                 '%s Home Page' % x for x in record.pop('homeurl')]
209     return records
210
211 def set_postaladdress(records):
212     # postalAddress defined in rfc4517
213     # homePostalAddress defined in ?
214     # streetAddress defined in rfc4519
215     for dn,record in records:
216         street = record.get('streetaddress', [None])[0]
217         addr2 = record.get('streetaddress2', [None])[0]
218         locality = record.get('localityname', [None])[0]
219         state = record.get('st', [None])[0]
220         if locality:
221             ls = locality
222             if state:
223                 ls += ', %s' % state
224         elif state:
225             ls = state
226         else:
227             ls = None
228         post = record.get('postalcode', [None])[0]
229         country = record.get('countryname', [None])[0]
230         if 'streetaddress2' in record:
231             record.pop('streetaddress2')
232         addr = '$'.join(
233             [line for line in [street, addr2, ls, post, country] if line])
234         _set_key(record, 'homepostaladdress', [addr], override=False)
235     return records
236
237
238 if __name__ == '__main__':
239     import argparse
240     import sys
241
242     p = argparse.ArgumentParser(description=__doc__)
243     p.add_argument(
244         '-b', '--basedn', dest='basedn', metavar='DNBASE',
245         default='ou=people,dc=example,dc=org',
246         help="Base distinguished name for the entries (%(default)s)")
247
248     args = p.parse_args()
249
250     text = sys.stdin.read()
251     text = cleanup(text, basedn=args.basedn)
252     sys.stdout.write(text)