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