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((get_itip_elements(value), attr)) 78 else: 79 d[name].append((value, attr)) 80 return d 81 82 def get_items(d, name, all=True): 83 if d.has_key(name): 84 values = d[name] 85 if not all and len(values) == 1: 86 return values[0] 87 else: 88 return values 89 else: 90 return None 91 92 def get_item(d, name): 93 return get_items(d, name, False) 94 95 def get_value_map(d, name): 96 items = get_items(d, name) 97 if items: 98 return dict(items) 99 else: 100 return {} 101 102 def get_values(d, name, all=True): 103 if d.has_key(name): 104 values = d[name] 105 if not all and len(values) == 1: 106 return values[0][0] 107 else: 108 return map(lambda x: x[0], values) 109 else: 110 return None 111 112 def get_value(d, name): 113 return get_values(d, name, False) 114 115 def get_address(value): 116 return value.startswith("mailto:") and value[7:] or value 117 118 def get_uri(value): 119 return value.startswith("mailto:") and value or "mailto:%s" % value 120 121 def handle_itip_part(part, recipients): 122 method = part.get_param("method") 123 124 # Decode the data and parse it. 125 126 f = StringIO(part.get_payload(decode=True)) 127 128 try: 129 doctype, attrs, elements = parse(f, encoding=part.get_content_charset()) 130 except (ParseError, ValueError): 131 sys.exit(EX_DATAERR) 132 133 # Only handle calendar information. 134 135 if doctype == "VCALENDAR": 136 itip = get_itip_elements(elements) 137 138 # Require consistency between declared and employed methods. 139 140 if get_value(itip, "METHOD") == method: 141 142 # Look for different kinds of sections. 143 144 for name, cls in handlers: 145 for details in get_values(itip, name) or []: 146 147 # Dispatch to a handler and obtain any response. 148 149 handler = cls(details, recipients) 150 part = methods[method](handler)() 151 152 if part: 153 print >>open("/tmp/imip.txt", "a"), part.as_string() 154 155 class Handler: 156 157 "General handler support." 158 159 def __init__(self, details, recipients): 160 161 """ 162 Initialise the handler with the 'details' of a calendar object and the 163 'recipients' of the object. 164 """ 165 166 self.details = details 167 self.recipients = set(recipients) 168 169 self.uid = get_value(details, "UID") 170 self.sequence = get_value(details, "SEQUENCE") 171 self.store = imip_store.FileStore() 172 173 def get_items(self, name, all=True): 174 return get_items(self.details, name, all) 175 176 def get_item(self, name): 177 return get_item(self.details, name) 178 179 def get_value_map(self, name): 180 return get_value_map(self.details, name) 181 182 def get_values(self, name, all=True): 183 return get_values(self.details, name, all) 184 185 def get_value(self, name): 186 return get_value(self.details, name) 187 188 def filter_by_recipients(self, values): 189 return self.recipients.intersection(map(get_address, values)) 190 191 class Event(Handler): 192 193 "An event handler." 194 195 def add(self): 196 pass 197 198 def cancel(self): 199 pass 200 201 def counter(self): 202 pass 203 204 def declinecounter(self): 205 pass 206 207 def publish(self): 208 pass 209 210 def refresh(self): 211 pass 212 213 def reply(self): 214 215 "Since this handler does not send requests, it will not handle replies." 216 217 pass 218 219 def request(self): 220 pass 221 222 class Freebusy(Handler): 223 224 "A free/busy handler." 225 226 def publish(self): 227 pass 228 229 def reply(self): 230 231 "Since this handler does not send requests, it will not handle replies." 232 233 pass 234 235 def request(self): 236 237 """ 238 Respond to a request by sending a reply containing free/busy information 239 for each indicated attendee. 240 """ 241 242 attendee_map = self.get_value_map("ATTENDEE") 243 organiser = self.get_item("ORGANIZER") 244 245 # Only provide details for recipients who are also attendees. 246 247 attendees = map(get_uri, self.filter_by_recipients(attendee_map)) 248 249 if not attendees and not organiser: 250 return 251 252 organiser, organiser_attr = organiser 253 254 # Get the details for the attendee. 255 256 out = StringIO() 257 try: 258 w = iterwrite(out, encoding="utf-8") 259 260 calendar = [] 261 cwrite = calendar.append 262 cwrite(("METHOD", {}, "REPLY")) 263 cwrite(("VERSION", {}, "2.0")) 264 265 for attendee in attendees: 266 freebusy = self.store.get_freebusy(attendee) 267 if freebusy: 268 record = [] 269 rwrite = record.append 270 rwrite(("ORGANIZER", organiser_attr, organiser)) 271 rwrite(("ATTENDEE", attendee_map[attendee], attendee)) 272 rwrite(("UID", {}, self.uid)) 273 274 for start, end in freebusy: 275 rwrite(("FREEBUSY", {}, [start, end])) 276 277 cwrite(("VFREEBUSY", {}, record)) 278 279 # Send a reply with the information. 280 281 w.write("VCALENDAR", {}, calendar) 282 283 return MIMEText(out.getvalue(), "calendar", "utf-8") 284 285 finally: 286 out.close() 287 288 class Journal(Handler): 289 290 "A journal entry handler." 291 292 def add(self): 293 pass 294 295 def cancel(self): 296 pass 297 298 def publish(self): 299 pass 300 301 class Todo(Handler): 302 303 "A to-do item handler." 304 305 def add(self): 306 pass 307 308 def cancel(self): 309 pass 310 311 def counter(self): 312 pass 313 314 def declinecounter(self): 315 pass 316 317 def publish(self): 318 pass 319 320 def refresh(self): 321 pass 322 323 def reply(self): 324 325 "Since this handler does not send requests, it will not handle replies." 326 327 pass 328 329 def request(self): 330 pass 331 332 # Handler registry. 333 334 handlers = [ 335 ("VFREEBUSY", Freebusy), 336 ("VEVENT", Event), 337 ("VTODO", Todo), 338 ("VJOURNAL", Journal), 339 ] 340 341 methods = { 342 "ADD" : lambda handler: handler.add, 343 "CANCEL" : lambda handler: handler.cancel, 344 "COUNTER" : lambda handler: handler.counter, 345 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 346 "PUBLISH" : lambda handler: handler.publish, 347 "REFRESH" : lambda handler: handler.refresh, 348 "REPLY" : lambda handler: handler.reply, 349 "REQUEST" : lambda handler: handler.request, 350 } 351 352 353 def main(): 354 355 # Obtain the different kinds of recipients. 356 357 original_recipients = [] 358 recipients = [] 359 360 l = [] 361 362 for arg in sys.argv[1:]: 363 if arg == "-o": 364 l = original_recipients 365 elif arg == "-r": 366 l = recipients 367 elif arg == "-d": 368 pass 369 else: 370 l.append(arg) 371 372 process(sys.stdin, original_recipients, recipients) 373 374 if __name__ == "__main__": 375 if "-d" in sys.argv[1:]: 376 main() 377 else: 378 try: 379 main() 380 except SystemExit, value: 381 sys.exit(value) 382 except Exception, exc: 383 type, value, tb = sys.exc_info() 384 print >>sys.stderr, "Exception %s at %d" % (exc, tb.tb_lineno) 385 sys.exit(EX_TEMPFAIL) 386 sys.exit(0) 387 388 # vim: tabstop=4 expandtab shiftwidth=4