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