1 #!/usr/bin/env python 2 3 from email import message_from_file 4 from email.mime.text import MIMEText 5 from smtplib import SMTP 6 from vCalendar import parse, iterwrite, ParseError, SECTION_TYPES 7 import imip_store 8 import sys 9 10 try: 11 from cStringIO import StringIO 12 except ImportError: 13 from StringIO import StringIO 14 15 # Postfix exit codes. 16 17 EX_USAGE = 64 18 EX_DATAERR = 65 19 EX_NOINPUT = 66 20 EX_NOUSER = 67 21 EX_NOHOST = 68 22 EX_UNAVAILABLE = 69 23 EX_SOFTWARE = 70 24 EX_OSERR = 71 25 EX_OSFILE = 72 26 EX_CANTCREAT = 73 27 EX_IOERR = 74 28 EX_TEMPFAIL = 75 29 EX_PROTOCOL = 76 30 EX_NOPERM = 77 31 EX_CONFIG = 78 32 33 # Permitted iTIP content types. 34 35 itip_content_types = [ 36 "text/calendar", # from RFC 6047 37 "text/x-vcalendar", "application/ics", # other possibilities 38 ] 39 40 # Sending of outgoing messages. 41 42 def sendmail(sender, recipients, data): 43 smtp = SMTP("localhost") 44 smtp.sendmail(sender, recipients, data) 45 smtp.quit() 46 47 # Processing of incoming messages. 48 49 def process(f, original_recipients, recipients): 50 51 """ 52 Process content from the stream 'f' accompanied by the given 53 'original_recipients' and 'recipients'. 54 """ 55 56 msg = message_from_file(f) 57 print >>open("/tmp/imip.txt", "a"), "----" 58 print >>open("/tmp/imip.txt", "a"), original_recipients, recipients 59 print >>open("/tmp/imip.txt", "a"), "----" 60 print >>open("/tmp/imip.txt", "a"), msg.as_string() 61 print >>open("/tmp/imip.txt", "a") 62 63 # Handle messages with iTIP parts. 64 65 for part in msg.walk(): 66 if part.get_content_type() in itip_content_types and \ 67 part.get_param("method"): 68 69 handle_itip_part(part, original_recipients) 70 71 def get_itip_elements(elements): 72 d = {} 73 for name, attr, value in elements: 74 if not d.has_key(name): 75 d[name] = [] 76 if name in SECTION_TYPES: 77 d[name].append((attr, get_itip_elements(value))) 78 else: 79 d[name].append((attr, value)) 80 return d 81 82 def get_attr_value(d, name, single=True): 83 if d.has_key(name): 84 values = d[name] 85 if single and len(values) == 1: 86 return values[0] 87 else: 88 return values 89 else: 90 return None 91 92 def get_value_map(d, name): 93 items = get_attr_value(d, name, False) 94 if items: 95 return map_from_list(items) 96 else: 97 return {} 98 99 def get_value(d, name, single=True): 100 if d.has_key(name): 101 values = d[name] 102 if single and len(values) == 1: 103 return values[0][1] 104 else: 105 return map(lambda x: x[1], values) 106 else: 107 return None 108 109 def map_from_list(l): 110 items = map(lambda (v, a): (a, v), l) 111 return dict(items) 112 113 def get_address(value): 114 return value.startswith("mailto:") and value[7:] or value 115 116 def get_uri(value): 117 return value.startswith("mailto:") and value or "mailto:%s" % value 118 119 def handle_itip_part(part, recipients): 120 method = part.get_param("method") 121 122 # Decode the data and parse it. 123 124 f = StringIO(part.get_payload(decode=True)) 125 126 try: 127 doctype, attrs, elements = parse(f, encoding=part.get_content_charset()) 128 except (ParseError, ValueError): 129 sys.exit(EX_DATAERR) 130 131 # Only handle calendar information. 132 133 if doctype == "VCALENDAR": 134 itip = get_itip_elements(elements) 135 136 # Require consistency between declared and employed methods. 137 138 if get_value(itip, "METHOD") == method: 139 140 # Look for different kinds of sections. 141 142 for name, cls in handlers: 143 for details in get_value(itip, name, False) or []: 144 145 # Dispatch to a handler and obtain any response. 146 147 handler = cls(details, recipients) 148 part = methods[method](handler)() 149 150 if part: 151 print >>open("/tmp/imip.txt", "a"), part.as_string() 152 153 class Handler: 154 155 "General handler support." 156 157 def __init__(self, details, recipients): 158 159 """ 160 Initialise the handler with the 'details' of a calendar object and the 161 'recipients' of the object. 162 """ 163 164 self.details = details 165 self.recipients = set(recipients) 166 167 self.uid = get_value(details, "UID") 168 self.sequence = get_value(details, "SEQUENCE") 169 self.store = imip_store.FileStore() 170 171 def get_attr_value(self, name, single=True): 172 return get_attr_value(self.details, name, single) 173 174 def get_value_map(self, name): 175 return get_value_map(self.details, name) 176 177 def get_value(self, name, single=True): 178 return get_value(self.details, name, single) 179 180 def filter_by_recipients(self, values): 181 return self.recipients.intersection(map(get_address, values)) 182 183 class Event(Handler): 184 185 "An event handler." 186 187 def add(self): 188 pass 189 190 def cancel(self): 191 pass 192 193 def counter(self): 194 pass 195 196 def declinecounter(self): 197 pass 198 199 def publish(self): 200 pass 201 202 def refresh(self): 203 pass 204 205 def reply(self): 206 207 "Since this handler does not send requests, it will not handle replies." 208 209 pass 210 211 def request(self): 212 pass 213 214 class Freebusy(Handler): 215 216 "A free/busy handler." 217 218 def publish(self): 219 pass 220 221 def reply(self): 222 223 "Since this handler does not send requests, it will not handle replies." 224 225 pass 226 227 def request(self): 228 229 """ 230 Respond to a request by sending a reply containing free/busy information 231 for each indicated attendee. 232 """ 233 234 attendee_map = self.get_value_map("ATTENDEE") 235 organiser = self.get_attr_value("ORGANIZER") 236 237 # Only provide details for recipients who are also attendees. 238 239 attendees = map(get_uri, self.filter_by_recipients(attendee_map)) 240 241 if not attendees and not organiser: 242 return 243 244 organiser_attr, organiser = organiser 245 246 # Get the details for the attendee. 247 248 out = StringIO() 249 try: 250 w = iterwrite(out, encoding="utf-8") 251 252 calendar = [] 253 cwrite = calendar.append 254 cwrite(("METHOD", {}, "REPLY")) 255 cwrite(("VERSION", {}, "2.0")) 256 257 for attendee in attendees: 258 freebusy = self.store.get_freebusy(attendee) 259 if freebusy: 260 record = [] 261 rwrite = record.append 262 rwrite(("ORGANIZER", organiser_attr, organiser)) 263 rwrite(("ATTENDEE", attendee_map[attendee], attendee)) 264 rwrite(("UID", {}, self.uid)) 265 266 for start, end in freebusy: 267 rwrite(("FREEBUSY", {}, [start, end])) 268 269 cwrite(("VFREEBUSY", {}, record)) 270 271 # Send a reply with the information. 272 273 w.write("VCALENDAR", {}, calendar) 274 275 return MIMEText(out.getvalue(), "calendar", "utf-8") 276 277 finally: 278 out.close() 279 280 class Journal(Handler): 281 282 "A journal entry handler." 283 284 def add(self): 285 pass 286 287 def cancel(self): 288 pass 289 290 def publish(self): 291 pass 292 293 class Todo(Handler): 294 295 "A to-do item handler." 296 297 def add(self): 298 pass 299 300 def cancel(self): 301 pass 302 303 def counter(self): 304 pass 305 306 def declinecounter(self): 307 pass 308 309 def publish(self): 310 pass 311 312 def refresh(self): 313 pass 314 315 def reply(self): 316 317 "Since this handler does not send requests, it will not handle replies." 318 319 pass 320 321 def request(self): 322 pass 323 324 # Handler registry. 325 326 handlers = [ 327 ("VFREEBUSY", Freebusy), 328 ("VEVENT", Event), 329 ("VTODO", Todo), 330 ("VJOURNAL", Journal), 331 ] 332 333 methods = { 334 "ADD" : lambda handler: handler.add, 335 "CANCEL" : lambda handler: handler.cancel, 336 "COUNTER" : lambda handler: handler.counter, 337 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 338 "PUBLISH" : lambda handler: handler.publish, 339 "REFRESH" : lambda handler: handler.refresh, 340 "REPLY" : lambda handler: handler.reply, 341 "REQUEST" : lambda handler: handler.request, 342 } 343 344 if __name__ == "__main__": 345 try: 346 # Obtain the different kinds of recipients. 347 348 original_recipients = [] 349 recipients = [] 350 351 l = [] 352 353 for arg in sys.argv[1:]: 354 if arg == "-o": 355 l = original_recipients 356 elif arg == "-r": 357 l = recipients 358 else: 359 l.append(arg) 360 361 process(sys.stdin, original_recipients, recipients) 362 except: 363 sys.exit(EX_SOFTWARE) 364 else: 365 sys.exit(0) 366 367 # vim: tabstop=4 expandtab shiftwidth=4