# $Id: contactlog.py,v 1.1 1998/07/31 03:59:59 connolly Exp $ # This code sort of works, at least to demonstrate python/Outlook # integration. I got through two of the categories of stylized # data that I keep in my Pilot Address book (caller-id info, # and business cards; the others are email, post, etc.) # Then I got bored. # Especially watch out for stuff marked with @@. # # But I'm documenting it carefully, # because I've been looking for these clues for # *months* and I don't want to lose them. # And I'm releasing it. Share and Enjoy. # Dan Connolly # # http://www.w3.org/People/Connolly/ # Copyright © 1998 World Wide Web Consortium, # (Massachusetts Institute of Technology, Institut National de # Recherche en Informatique et en Automatique, Keio University). All # Rights Reserved. # # Permission to use, copy, modify, and distribute this software # and its documentation for any purpose and without fee or # royalty is hereby granted, per the terms and conditions in # # W3C IPR SOFTWARE NOTICE # http://www.w3.org/COPYRIGHT # September 1997 # This module depends on standard and built-in python modules, per # Python Library Reference # April 14, 1998 # Release 1.5.1 # http://www.python.org/doc/lib/lib.html import string, StringIO # It also uses the win32com package # maintained by Mark Hammond # http://www.python.org/windows/win32com/ # # specifically, as documented in # PythonWin Help # Help file built: 04/26/98 # available in http://www.python.org/windows/win32all/win32all.exe # Here we bind to the the actual Outlook 98 runtime, # whose interface is documented in a # Microsoft help file, "Microsoft Outlook Visual Basic" # # I found the help file via the Microsoft Knowledge Base: # # "OL98: How to Install Visual Basic Help" # Last reviewed: April 1, 1998 # Article ID: Q183220 # http://support.microsoft.com/support/kb/articles/q183/2/20.asp # # The following lines are generated by makepy # #Outlook 98 Type Library # {00062FFF-0000-0000-C000-000000000046}, lcid=0, major=8, minor=5 # Use these commands in Python code to auto generate .py support from win32com.client import gencache outlook = gencache.EnsureModule('{00062FFF-0000-0000-C000-000000000046}', 0, 8, 5) # The only namespace supported in Outlook '98 # see "Microsoft Outlook Visual Basic" MAPI = "MAPI" # Hard-coded configuration # probably should use sys.argv, but I didn't bother... Inf = "C:\\winnt\\profiles\\connolly\\desktop\\980728pilot-addr.txt" # for other folks to do testing, here are a couple lines from that file: _test_data = \ """Last Name First Name Job Title Company Business Phone Home Phone Business Fax Other Phone E-mail Street Address City State Zip/Postal Code Country Location Birthday User Field 1 User Field 2 User Field 3 Private Categories "Abrahamson" "Dr. David M" "+353-1-608-1716" "cavid@cs.tcd.ie" "Trinity College" "Dublin 2, Ireland" "1996-11" "lunch 96-11-13 ISO HTML Boston lunch" "0" "Business Card" """ def main(): oapp = Pilot2Outlook() #infp = open(Inf) infp = StringIO.StringIO(_test_data) i = 0 while 1: row = nextrow(infp) if not row: break i = i + 1 if i > 500: break #@@ can't figure out how # to break a runaway script in PythonWin, # so I added this little hack print "@@add:", row oapp.addPilotAddress(row) # outlook.Application is the Application class, per # the type library and help file cited above. # # It integrates completely seamlessly into the python # object model. # # Hence, for methods such as GetNamespace, see # the help file. I found the following article quite # helpful as well. The examples are in visual basic, but # the translation to python is straightforward. # # The Microsoft Outlook 97 Automation Server Programming Model # Last Updated: June 26, 1998 # http://www.microsoft.com/OutlookDev/Articles/Outprog.htm # class Pilot2Outlook(outlook.Application): def findContact(self, filter): contacts = self.GetNamespace(MAPI).GetDefaultFolder(outlook.OlDefaultFolders.olFolderContacts) return contacts.Items.Find(filter) def incomingCall(self, name, num, isotime): # convert caller-id format: 333-555-1212 # to Outlook format: (333) 555-1212 if len(num) == 12: num = "(%s) %s" % (num[:3], num[4:]) ji = self.CreateItem(outlook.OlItemType.olJournalItem) ji.Type = "Phone call" # set icon? # stuff the original caller-id data away somewhere # I'm not confident this UserProperties code # works... wierdness around byRef args or something. # I could never # find the userproperties for a journal item # in the Outlook GUI to check one way or the other. p = ji.UserProperties.Add("CallerIDName", outlook.OlUserPropertyType.olText) p.Value = name p = ji.UserProperties.Add("CallerIDNumber", outlook.OlUserPropertyType.olText) p.Value = num #@@ mobile, other phone numbers... contact = self.findContact('[Business Phone] = "%s" or [Home Phone] = "%s"' % (num, num)) if contact: if contact.FullName: name = contact.FullName ji.Companies = contact.CompanyName else: contact = self.CreateItem(outlook.OlItemType.olContactItem) contact.FullName = name contact.BusinessTelephoneNumber = num contact.Save() ji.Recipients.Add(name) t = iso2vb(isotime) ji.Start = t ji.Subject = "Call from %s %s" % (name or num, t) ji.Save() #ji.Display() #@@ return ji, contact def logBusinessCard(self, family,given,title,company,work,fax,email, street,city,state,zip,country,isodate,note): contact = self.findContact( '[Last Name] = "%s" and [First Name] = "%s" and [Company] = "%s"' % (family, given, company)) if not contact: print "@@ new contact" contact = self.CreateItem(outlook.OlItemType.olContactItem) contact.LastName = family contact.FirstName = given contact.CompanyName = company contact.JobTitle = title contact.BusinessTelephoneNumber = work contact.BusinessFaxNumber = fax contact.BusinessAddressStreet = street contact.BusinessAddressCity = city contact.BusinessAddressState = state contact.BusinessAddressPostalCode = zip contact.BusinessAddressCountry = country #@@ don't clobber email contact.Email1Address = email if isodate: ji = self.CreateItem(outlook.OlItemType.olJournalItem) ji.Start = iso2vb(isodate) ji.Type = "Conversation" # or perhaps meeting? ji.Recipients.Add(contact.FullName) ji.Companies = company ji.Body = note ji.Subject = "Business Card %s %s" % (contact.FullName, company) #ji.Display() #@@ #@@ include scanned image of card? ji.Save() else: ji = None if note: if contact.Body: contact.Body = contact.Body + '\n' + note else: contact.Body = note contact.Save() return ji, contact def addPilotAddress(self, row): family,given,title,company,work,home,fax,other,email, \ street,city,state,zip,country,airport,birthday, \ updated,expired,note,private,category = row if category == "Caller-id": ji, c = self.incomingCall(family, work, updated) if note: ji.Body = note #@@ translate private to Sensitivity ji.Save() if given or title or company or home \ or fax or other or email or street \ or city or state or zip or country or airport \ or birthday or expired: print "@@info besides family, work, updated, note, private:",\ row elif category == "Business Card": if updated[-5:] == " card": updated = updated[:-5] ji, c = self.logBusinessCard(family,given,title,company, work,fax,email, street,city,state,zip,country,updated,note) ji.Display() #@@debugging c.Display() #@@ other fields: home phone, airport? # birthday, expired, private else: print "@@category not yet supported:", category, family, given, company def iso2vb(isotime): # YYYY-MM-DDTHH:MM:SS # raises ??? exception when parts are missing? year = isotime[:4] month = isotime[5:7] day = isotime[8:10] or '1' ## @# only month given hour = isotime[11:13] minute = isotime[14:16] second = isotime[17:19] ret = month + '/' + day + '/' + year if hour and minute: ret = ret + ' ' + hour + ':' + minute if second: ret = ret + ':' + second print "@@convdate: ", isotime, ret return ret def nextrow(fp): # A simple state-machine implementation of Windows # tab-delimited file format with quoted fields. # I'm not sure if this really # does CRLF's right. # There are faster ways to do this, I'm sure... s = 'start' row = () field = "" while 1: c = fp.read(1) if not c: if field or len(row): raise IOError, "bad end of file" return None if s == 'start': if c == "\n": row = row + (field,) return row elif c == "\t": row = row + (field,) field = '' elif c == '"': s = 'quoted' else: field = field + c elif s == 'quoted': if c == '"': s = 'start' elif c == "\\": s = 'escaped' else: field = field + c elif s == 'escaped': field = field + c s = 'quoted' else: raise RuntimeError, 'bad case' if __name__ == '__main__': main()