1import PHPDeserializer 2import webdavlib 3import sys 4 5commonMappings = { "owner_id": "owner", 6 "object_id": "filename", 7 "object_uid": "uid", 8 "object_name": "fn" } 9cardMappings = { "object_alias": "nickname", 10 "object_email": "email", 11 "object_homeaddress": "homeaddress", 12 "object_homephone": "homephone", 13 "object_workaddress": "workaddress", 14 "object_workphone": "workphone", 15 "object_cellphone": "cellphone", 16 "object_fax": "fax", 17 "object_title": "title", 18 "object_company": "org", 19 "object_notes": "notes", 20 "object_freebusyurl": "fburl" } 21 22prodid = "-//Inverse inc.//SOGo Turba Importer 1.0//EN" 23 24# a managed type of template where each line is put only if at least one field 25# has been filled 26cardTemplate = u"""BEGIN:VCARD\r 27VERSION:3.0\r 28PRODID:%s\r 29UID:${uid}\r 30FN:${fn}\r 31TITLE:${title}\r 32ORG:${org};\r 33NICKNAME:${nickname}\r 34EMAIL:${email}\r 35ADR;TYPE=work:;;${workaddress};;;;\r 36ADR;TYPE=home:;;${homeaddress};;;;\r 37TEL;TYPE=work:${workphone}\r 38TEL;TYPE=home:${homephone}\r 39TEL;TYPE=fax:${fax}\r 40NOTE:${notes}\r 41FBURL:${fburl}\r 42END:VCARD""" % prodid 43 44class TurbaConverter: 45 def __init__(self, user, webdavConfig): 46 self.user = user 47 self.webdavConfig = webdavConfig 48 49 def start(self, conn): 50 self.conn = conn 51 self.readUsers() 52 self.missing = [] 53 for user in self.users.keys(): 54 self.hasCards = False 55 self.hasLists = False 56 self.currentUser = user 57 self.readUserRecords() 58 if self.hasCards or self.hasLists: 59 print "Converting addressbook of '%s'" % user 60 self.prepareCards() 61 self.uploadCards() 62 self.prepareLists() 63 self.uploadLists() 64 else: 65 self.missing.append(user) 66 67 if len(self.missing) > 0: 68 print "No information extracted for: %s" % ", ".join(self.missing) 69 70 print "Done" 71 72 def readUsers(self): 73 self.users = {} 74 75 cursor = self.conn.cursor() 76 query = "SELECT user_uid, datatree_name FROM horde_datatree" 77 if self.user != "ALL": 78 query = query + " WHERE user_uid = '%s'" % self.user 79 cursor.execute(query) 80 81 records = cursor.fetchall() 82 count = 0 83 max = len(records) 84 for record in records: 85 record_user = record[0].lower() 86 if not self.users.has_key(record_user): 87 self.users[record_user] = [] 88 self.users[record_user].append(record[1]) 89 count = count + 1 90 cursor.close() 91 92 def readUserRecords(self): 93 self.cards = {} 94 self.lists = {} 95 96 cursor = self.conn.cursor() 97 owner_ids = self.users[self.currentUser] 98 whereClause = "owner_id = '%s'" % "' or owner_id = '".join(owner_ids) 99 query = "SELECT * FROM turba_objects WHERE %s" % whereClause 100 cursor.execute(query) 101 self.prepareColumns(cursor) 102 103 records = cursor.fetchall() 104 count = 0 105 max = len(records) 106 while count < max: 107 self.parseRecord(records[count]) 108 count = count + 1 109 110 cursor.close() 111 112 def prepareColumns(self, cursor): 113 self.columns = {} 114 count = 0 115 for dbColumn in cursor.description: 116 columnId = dbColumn[0] 117 self.columns[columnId] = count 118 count = count + 1 119 120 def parseRecord(self, record): 121 typeCol = self.columns["object_type"] 122 meta = {} 123 self.applyRecordMappings(meta, record, commonMappings) 124 125 if record[typeCol] == "Object": 126 meta["type"] = "card" 127 self.hasCards = True 128 self.applyRecordMappings(meta, record, cardMappings) 129 elif record[typeCol] == "Group": 130 meta["type"] = "list" 131 self.hasLists = True 132 self.fillListMembers(meta, record) 133 else: 134 raise Exception, "UNKNOWN TYPE: %s" % record[type] 135 136 self.dispatchMeta(meta) 137 138 def applyRecordMappings(self, meta, record, mappings): 139 for k in mappings.keys(): 140 metaKey = mappings[k] 141 meta[metaKey] = self.recordColumn(record, k) 142 143 def recordColumn(self, record, columnName): 144 columnIndex = self.columns[columnName] 145 value = record[columnIndex] 146 if value is None: 147 value = u"" 148 else: 149 value = self.deUTF8Ize(value) 150 151 return value 152 153 def deUTF8Ize(self, value): 154 # unicode -> repeat(utf-8 str -> iso-8859-1 str) -> unicode 155 oldValue = value 156 157 done = False 158 while not done: 159 try: 160 utf8Value = value.encode("iso-8859-1") 161 value = utf8Value.decode("utf-8") 162 except: 163 done = True 164 if value == oldValue: 165 done = True 166 167 return value 168 169 def fillListMembers(self, meta, record): 170 members = self.recordColumn(record, "object_members") 171 if members is not None and len(members) > 0: 172 deserializer = PHPDeserializer.PHPDeserializer(members) 173 dMembers = deserializer.deserialize() 174 else: 175 dMembers = [] 176 meta["members"] = dMembers 177 178 def dispatchMeta(self, meta): 179 owner = meta["owner"] 180 if meta["type"] == "card": 181 repository = self.cards 182 else: 183 repository = self.lists 184 filename = meta["filename"] 185 repository[filename] = meta 186 187 def prepareCards(self): 188 count = 0 189 for filename in self.cards.keys(): 190 card = self.cards[filename] 191 card["data"] = self.buildVCard(card).encode("utf-8") 192 count = count + 1 193 if count > 0: 194 print " prepared %d cards" % count 195 196 def buildVCard(self, card): 197 vcardArray = [] 198 tmplArray = cardTemplate.split("\r\n") 199 for line in tmplArray: 200 keyPos = line.find("${") 201 if keyPos > -1: 202 keyEndPos = line.find("}") 203 key = line[keyPos+2:keyEndPos] 204 if card.has_key(key): 205 value = card[key] 206 if len(value) > 0: 207 newLine = "%s%s%s" % (line[0:keyPos], 208 value.replace(";", "\;"), 209 line[keyEndPos + 1:]) 210 vcardArray.append(self.foldLineIfNeeded(newLine)) 211 else: 212 vcardArray.append(self.foldLineIfNeeded(line)) 213 214 return "\r\n".join(vcardArray) 215 216 def foldLineIfNeeded(self, line): 217 wasFolded = False 218 newLine = line\ 219 .replace("\\", "\\\\") \ 220 .replace("\r", "\\r") \ 221 .replace("\n", "\\n") 222 lines = [] 223 while len(newLine) > 73: 224 wasFolded = True 225 lines.append(newLine[0:73]) 226 newLine = newLine[73:] 227 lines.append(newLine) 228 229 newLine = "\r\n ".join(lines) 230 if wasFolded: 231 print "line was folded: '%s' ->\n\n%s\n\n" % (line, newLine) 232 233 return newLine 234 235 def uploadCards(self): 236 self.uploadEntries(self.cards, 237 "vcf", "text/x-vcard; charset=utf-8"); 238 239 def prepareLists(self): 240 count = 0 241 skipped = 0 242 for filename in self.lists.keys(): 243 list = self.lists[filename] 244 vlist = self.buildVList(list) 245 if vlist is None: 246 skipped = skipped + 1 247 else: 248 list["data"] = vlist.encode("utf-8") 249 count = count + 1 250 251 if (count + skipped) > 0: 252 print " prepared %d lists. %d were skipped." % (count, skipped) 253 254 def buildVList(self, list): 255 vlist = None 256 257 members = list["members"] 258 if len(members) > 0: 259 cardMembers = [] 260 for member in members: 261 card = self.getListCard(member) 262 if card is not None: 263 cardMembers.append(card) 264 if len(cardMembers) > 0: 265 vlist = self.assembleVList(list, cardMembers) 266 else: 267 print " list '%s' skipped because of lack of usable" \ 268 " members" % list["filename"] 269 270 return vlist 271 272 def getListCard(self, cardRef): 273 card = None 274 if len(cardRef) != 0 and not cardRef.startswith("localldap:"): 275 if cardRef.startswith("localsql:"): 276 cardRef = cardRef[9:] 277 if self.cards.has_key(cardRef): 278 card = self.cards[cardRef] 279 else: 280 print "card reference does not exist: '%s'" % cardRef 281 282 return card 283 284 def assembleVList(self, list, cardMembers): 285 entries = [] 286 for cardMember in cardMembers: 287 if cardMember.has_key("fn") and len(cardMember["fn"]) > 0: 288 fn = ";FN=%s" % cardMember["fn"] 289 else: 290 fn = "" 291 if cardMember.has_key("email") and len(cardMember["email"]) > 0: 292 email = ";EMAIL=%s" % cardMember["email"] 293 else: 294 email = "" 295 entries.append("CARD%s%s:%s.vcf" 296 % (fn, email, cardMember["filename"])) 297 if list.has_key("fn") and len(list["fn"]) > 0: 298 listfn = "FN:%s\r\n" % list["fn"] 299 else: 300 listfn = "" 301 vlist = """BEGIN:VLIST\r 302PRODID:%s\r 303VERSION:1.0\r 304UID:%s\r 305%s%s\r 306END:VLIST""" % (prodid, list["uid"], listfn, "\r\n".join(entries)) 307 308 return vlist 309 310 def uploadLists(self): 311 self.uploadEntries(self.lists, 312 "vlf", "text/x-vcard; charset=utf-8"); 313 314 def uploadEntries(self, entries, extension, mimeType): 315 isatty = sys.stdout.isatty() # enable progressive display of summary 316 success = 0 317 failure = 0 318 client = webdavlib.WebDAVClient(self.webdavConfig["hostname"], 319 self.webdavConfig["port"], 320 self.webdavConfig["username"], 321 self.webdavConfig["password"]) 322 collection = '/SOGo/dav/%s/Contacts/personal' % self.currentUser 323 324 mkcol = webdavlib.WebDAVMKCOL(collection) 325 client.execute(mkcol) 326 327 for entryName in entries.keys(): 328 entry = entries[entryName] 329 if entry.has_key("data"): 330 fullFilename = "%s.%s" % (entry["filename"], extension) 331 url = "%s/%s" % (collection, fullFilename) 332 put = webdavlib.HTTPPUT(url, entry["data"]) 333 put.content_type = mimeType 334 client.execute(put) 335 if (put.response["status"] < 200 336 or put.response["status"] > 399): 337 failure = failure + 1 338 print " error uploading '%s': %d" \ 339 % (fullFilename, put.response["status"]) 340 else: 341 success = success + 1 342 if isatty: 343 print "\r successes: %d; failures: %d" % (success, failure), 344 if (success + failure) % 5 == 0: 345 sys.stdout.flush() 346 if isatty: 347 print "" 348 else: 349 if (success + failure) > 0: 350 print " successes: %d; failures: %d\n" % (success, failure) 351 sys.stdout.flush() 352