1 #!/usr/bin/env python 2 3 """ 4 Interpretation and preparation of iMIP content, together with a content handling 5 mechanism employed by specific recipients. 6 """ 7 8 from datetime import date, datetime 9 from email.mime.text import MIMEText 10 from pytz import timezone, UnknownTimeZoneError 11 from vCalendar import parse, ParseError, to_dict 12 import imip_store 13 import re 14 15 try: 16 from cStringIO import StringIO 17 except ImportError: 18 from StringIO import StringIO 19 20 # iCalendar date and datetime parsing (from DateSupport in MoinSupport). 21 22 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})' 23 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ 24 ur'(?:' \ 25 ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \ 26 ur'(?P<utc>Z)?' \ 27 ur')?' 28 29 match_date_icalendar = re.compile(date_icalendar_regexp_str, re.UNICODE).match 30 match_datetime_icalendar = re.compile(datetime_icalendar_regexp_str, re.UNICODE).match 31 32 # Content interpretation. 33 34 def get_items(d, name, all=True): 35 36 """ 37 Get all items from 'd' with the given 'name', returning single items if 38 'all' is specified and set to a false value and if only one value is 39 present for the name. Return None if no items are found for the name. 40 """ 41 42 if d.has_key(name): 43 values = d[name] 44 if not all and len(values) == 1: 45 return values[0] 46 else: 47 return values 48 else: 49 return None 50 51 def get_item(d, name): 52 return get_items(d, name, False) 53 54 def get_value_map(d, name): 55 56 """ 57 Return a dictionary for all items in 'd' having the given 'name'. The 58 dictionary will map values for the name to any attributes or qualifiers 59 that may have been present. 60 """ 61 62 items = get_items(d, name) 63 if items: 64 return dict(items) 65 else: 66 return {} 67 68 def get_values(d, name, all=True): 69 if d.has_key(name): 70 values = d[name] 71 if not all and len(values) == 1: 72 return values[0][0] 73 else: 74 return map(lambda x: x[0], values) 75 else: 76 return None 77 78 def get_value(d, name): 79 return get_values(d, name, False) 80 81 def get_utc_datetime(d, name): 82 value, attr = get_item(d, name) 83 dt = get_datetime(value, attr) 84 return to_utc_datetime(dt) 85 86 def to_utc_datetime(dt): 87 if not dt: 88 return None 89 elif isinstance(dt, datetime): 90 return dt.astimezone(timezone("UTC")) 91 else: 92 return dt 93 94 def format_datetime(dt): 95 if not dt: 96 return None 97 elif isinstance(dt, datetime): 98 return dt.strftime("%Y%m%dT%H%M%SZ") 99 else: 100 return dt.strftime("%Y%m%d") 101 102 def get_address(value): 103 return value.startswith("mailto:") and value[7:] or value 104 105 def get_uri(value): 106 return value.startswith("mailto:") and value or "mailto:%s" % value 107 108 def get_datetime(value, attr): 109 try: 110 tz = attr.has_key("TZID") and timezone(attr["TZID"]) or None 111 except UnknownTimeZoneError: 112 tz = None 113 114 if attr.get("VALUE") in (None, "DATE-TIME"): 115 m = match_datetime_icalendar(value) 116 if m: 117 dt = datetime( 118 int(m.group("year")), int(m.group("month")), int(m.group("day")), 119 int(m.group("hour")), int(m.group("minute")), int(m.group("second")) 120 ) 121 122 # Impose the indicated timezone. 123 # NOTE: This needs an ambiguity policy for DST changes. 124 125 tz = m.group("utc") and timezone("UTC") or tz or None 126 if tz is not None: 127 return tz.localize(dt) 128 else: 129 return dt 130 131 if attr.get("VALUE") == "DATE": 132 m = match_date_icalendar(value) 133 if m: 134 return date( 135 int(m.group("year")), int(m.group("month")), int(m.group("day")) 136 ) 137 return None 138 139 # Handler mechanism objects. 140 141 def handle_itip_part(part, recipients, handlers): 142 143 """ 144 Handle the given iTIP 'part' for the given 'recipients' using the given 145 'handlers'. 146 """ 147 148 method = part.get_param("method") 149 150 # Decode the data and parse it. 151 152 f = StringIO(part.get_payload(decode=True)) 153 154 itip = parse_object(f, part.get_content_charset(), "VCALENDAR") 155 156 # Ignore the part if not a calendar object. 157 158 if not itip: 159 return [] 160 161 # Only handle calendar information. 162 163 all_parts = [] 164 165 # Require consistency between declared and employed methods. 166 167 if get_value(itip, "METHOD") == method: 168 169 # Look for different kinds of sections. 170 171 all_objects = [] 172 173 for name, cls in handlers: 174 for details in get_values(itip, name) or []: 175 176 # Dispatch to a handler and obtain any response. 177 178 handler = cls(details, recipients) 179 object = methods[method](handler)() 180 181 # Concatenate responses for a single calendar object. 182 183 if object: 184 all_objects += object 185 186 # Obtain a message part for the objects. 187 188 if all_objects: 189 all_parts.append(to_part(response_methods[method], all_objects)) 190 191 return all_parts 192 193 def parse_object(f, encoding, objtype): 194 195 """ 196 Parse the iTIP content from 'f' having the given 'encoding'. Return None if 197 the content was not readable or suitable. 198 """ 199 200 try: 201 try: 202 doctype, attrs, elements = obj = parse(f, encoding=encoding) 203 if doctype == objtype: 204 return to_dict(obj)[objtype][0] 205 finally: 206 f.close() 207 except (ParseError, ValueError): 208 pass 209 210 return None 211 212 def to_part(method, calendar): 213 214 """ 215 Write using the given 'method', the 'calendar' details to a MIME 216 text/calendar part. 217 """ 218 219 encoding = "utf-8" 220 out = StringIO() 221 try: 222 imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding) 223 part = MIMEText(out.getvalue(), "calendar", encoding) 224 part.set_param("method", method) 225 return part 226 227 finally: 228 out.close() 229 230 class Handler: 231 232 "General handler support." 233 234 def __init__(self, details, recipients): 235 236 """ 237 Initialise the handler with the 'details' of a calendar object and the 238 'recipients' of the object. 239 """ 240 241 self.details = details 242 self.recipients = set(recipients) 243 244 self.uid = get_value(details, "UID") 245 self.sequence = get_value(details, "SEQUENCE") 246 self.dtstamp = get_value(details, "DTSTAMP") 247 248 self.store = imip_store.FileStore() 249 250 try: 251 self.publisher = imip_store.FilePublisher() 252 except OSError: 253 self.publisher = None 254 255 def get_items(self, name, all=True): 256 return get_items(self.details, name, all) 257 258 def get_item(self, name): 259 return get_item(self.details, name) 260 261 def get_value_map(self, name): 262 return get_value_map(self.details, name) 263 264 def get_values(self, name, all=True): 265 return get_values(self.details, name, all) 266 267 def get_value(self, name): 268 return get_value(self.details, name) 269 270 def get_utc_datetime(self, name): 271 return get_utc_datetime(self.details, name) 272 273 def filter_by_recipients(self, values): 274 return self.recipients.intersection(map(get_address, values)) 275 276 def require_organiser_and_attendees(self): 277 278 """ 279 Return the organiser and attendees for the current object, filtered by 280 the recipients of interest. Return None if no identities are eligible. 281 """ 282 283 attendee_map = self.get_value_map("ATTENDEE") 284 organiser = self.get_item("ORGANIZER") 285 286 # Only provide details for recipients who are also attendees. 287 288 attendees = {} 289 for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): 290 attendees[attendee] = attendee_map[attendee] 291 292 if not attendees and not organiser: 293 return None 294 295 return organiser, attendees 296 297 def have_new_object(self, attendee, objtype): 298 299 """ 300 Return whether the current object is new to the 'attendee' for the 301 given 'objtype'. 302 """ 303 304 f = self.store.get_event(attendee, self.uid) 305 event = f and parse_object(f, "utf-8", objtype) 306 307 # If found, compare SEQUENCE and potentially DTSTAMP. 308 309 if event: 310 sequence = get_value(event, "SEQUENCE") 311 dtstamp = get_value(event, "DTSTAMP") 312 313 # If the request refers to an older version of the event, ignore 314 # it. 315 316 old_dtstamp = self.dtstamp < dtstamp 317 318 if sequence is not None and ( 319 int(self.sequence) < int(sequence) or 320 int(self.sequence) == int(sequence) and old_dtstamp 321 ) or old_dtstamp: 322 323 return False 324 325 return True 326 327 # Handler registry. 328 329 methods = { 330 "ADD" : lambda handler: handler.add, 331 "CANCEL" : lambda handler: handler.cancel, 332 "COUNTER" : lambda handler: handler.counter, 333 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 334 "PUBLISH" : lambda handler: handler.publish, 335 "REFRESH" : lambda handler: handler.refresh, 336 "REPLY" : lambda handler: handler.reply, 337 "REQUEST" : lambda handler: handler.request, 338 } 339 340 response_methods = { 341 "REQUEST" : "REPLY", 342 } 343 344 # vim: tabstop=4 expandtab shiftwidth=4