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