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