5 """Cleanup the LDIF output from abook_ using `python-ldap`_.
7 .. _abook: http://abook.sourceforge.net/
8 .. _python-ldap: http://www.python-ldap.org/
17 def cleanup(text, basedn):
18 # pre-parser formatting
19 text = remove_trailing_mail(text)
20 text = remove_cn_commas(text)
22 records = ldif.ParseLDIF(StringIO.StringIO(text))
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)
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()
46 def remove_trailing_mail(text):
48 >>> print(remove_trailing_mail('\\n'.join([
50 ... 'dn: cn=John Doe,mail=',
55 dn: cn=John Doe,mail=x@y.com
59 return re.sub(',mail=$', ',mail=x@y.com', text, flags=re.MULTILINE)
61 def _sub_cn_commas(match):
62 cn = match.group(1).replace(',', '_')
63 return 'cn=%s,mail=' % cn
65 def remove_cn_commas(text):
67 >>> print(remove_cn_commas('\\n'.join([
69 ... 'dn: cn=John, Jane, and Jim Doe,mail=x@y.com',
70 ... 'cn: John, Jane, and Jim Doe',
74 dn: cn=John_ Jane_ and Jim Doe,mail=x@y.com
75 cn: John, Jane, and Jim Doe
78 return re.sub('cn=(.*),mail=', _sub_cn_commas, text)
80 def remove_empty_mail(records):
81 for dn,record in records:
82 if 'mail' in record and record['mail'] == ['']:
86 def remove_top_objectclass(records):
87 for dn,record in records:
88 if 'top' in record['objectclass']:
89 record['objectclass'].remove('top')
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
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)
107 def _set_key(record, key, value, override=True):
108 """Case-agnostic value setter.
110 >>> record = {'aB': 'old'}
111 >>> _set_key(record, 'AB', 'new')
115 keys = [k for k in record.keys() if k.lower() == key.lower()]
120 if override or k not in record:
123 def add_names(records):
125 Surname and givenName are defined in `RFC 4519`_.
127 .. _RFC 4512: http://tools.ietf.org/html/rfc4519
129 for dn,record in records:
131 gn,sn = cn[0].rsplit(' ', 1)
132 _set_key(record, 'sn', [sn], override=False)
133 _set_key(record, 'givenName', [gn], override=False)
136 def standardize_phone_numbers(records):
137 """Standardize phone numbers to match `E.123`_ international notation
139 Assumes numbers not starting with a '+' live in the USA.
143 ... ('cn=John', {'homephone': '123-456-7890'},
144 ... ('cn=Jane', {TODO})]
145 >>> pprint.pprint(standardize_phone_numbers(records))
147 .. _E.123: http://en.wikipedia.org/wiki/E.123
152 def standardize_country_code(records):
155 # http://tools.ietf.org/html/rfc4519
156 # http://tools.ietf.org/html/rfc4517
169 for dn,record in records:
170 if 'countryname' in record:
171 record['countryname'] = [
172 table.get(c, c) for c in record['countryname']]
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')
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')
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')
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')
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')]
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]
228 post = record.get('postalcode', [None])[0]
229 country = record.get('countryname', [None])[0]
230 if 'streetaddress2' in record:
231 record.pop('streetaddress2')
233 [line for line in [street, addr2, ls, post, country] if line])
234 _set_key(record, 'homepostaladdress', [addr], override=False)
238 if __name__ == '__main__':
242 p = argparse.ArgumentParser(description=__doc__)
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)")
248 args = p.parse_args()
250 text = sys.stdin.read()
251 text = cleanup(text, basedn=args.basedn)
252 sys.stdout.write(text)