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'. Return a list of responses, each response being a tuple of the 146 form (is-outgoing, message-part). 147 """ 148 149 method = part.get_param("method") 150 151 # Decode the data and parse it. 152 153 f = StringIO(part.get_payload(decode=True)) 154 155 itip = parse_object(f, part.get_content_charset(), "VCALENDAR") 156 157 # Ignore the part if not a calendar object. 158 159 if not itip: 160 return [] 161 162 # Require consistency between declared and employed methods. 163 164 if get_value(itip, "METHOD") == method: 165 166 # Look for different kinds of sections. 167 168 all_results = [] 169 170 for name, cls in handlers: 171 for details in get_values(itip, name) or []: 172 173 # Dispatch to a handler and obtain any response. 174 175 handler = cls(details, recipients) 176 result = methods[method](handler)() 177 178 # Concatenate responses for a single calendar object. 179 180 if result: 181 response_method, part = result 182 outgoing = method != response_method 183 all_results.append((outgoing, part)) 184 185 return all_results 186 187 return [] 188 189 def parse_object(f, encoding, objtype): 190 191 """ 192 Parse the iTIP content from 'f' having the given 'encoding'. Return None if 193 the content was not readable or suitable. 194 """ 195 196 try: 197 try: 198 doctype, attrs, elements = obj = parse(f, encoding=encoding) 199 if doctype == objtype: 200 return to_dict(obj)[objtype][0] 201 finally: 202 f.close() 203 except (ParseError, ValueError): 204 pass 205 206 return None 207 208 def to_part(method, calendar): 209 210 """ 211 Write using the given 'method', the 'calendar' details to a MIME 212 text/calendar part. 213 """ 214 215 encoding = "utf-8" 216 out = StringIO() 217 try: 218 imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding) 219 part = MIMEText(out.getvalue(), "calendar", encoding) 220 part.set_param("method", method) 221 return part 222 223 finally: 224 out.close() 225 226 class Handler: 227 228 "General handler support." 229 230 def __init__(self, details, recipients): 231 232 """ 233 Initialise the handler with the 'details' of a calendar object and the 234 'recipients' of the object. 235 """ 236 237 self.details = details 238 self.recipients = set(recipients) 239 240 self.uid = get_value(details, "UID") 241 self.sequence = get_value(details, "SEQUENCE") 242 self.dtstamp = get_value(details, "DTSTAMP") 243 244 self.store = imip_store.FileStore() 245 246 try: 247 self.publisher = imip_store.FilePublisher() 248 except OSError: 249 self.publisher = None 250 251 def get_items(self, name, all=True): 252 return get_items(self.details, name, all) 253 254 def get_item(self, name): 255 return get_item(self.details, name) 256 257 def get_value_map(self, name): 258 return get_value_map(self.details, name) 259 260 def get_values(self, name, all=True): 261 return get_values(self.details, name, all) 262 263 def get_value(self, name): 264 return get_value(self.details, name) 265 266 def get_utc_datetime(self, name): 267 return get_utc_datetime(self.details, name) 268 269 def filter_by_recipients(self, values): 270 return self.recipients.intersection(map(get_address, values)) 271 272 def require_organiser_and_attendees(self): 273 274 """ 275 Return the organiser and attendees for the current object, filtered by 276 the recipients of interest. Return None if no identities are eligible. 277 """ 278 279 attendee_map = self.get_value_map("ATTENDEE") 280 organiser = self.get_item("ORGANIZER") 281 282 # Only provide details for recipients who are also attendees. 283 284 attendees = {} 285 for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): 286 attendees[attendee] = attendee_map[attendee] 287 288 if not attendees and not organiser: 289 return None 290 291 return organiser, attendees 292 293 def have_new_object(self, attendee, objtype): 294 295 """ 296 Return whether the current object is new to the 'attendee' for the 297 given 'objtype'. 298 """ 299 300 f = self.store.get_event(attendee, self.uid) 301 event = f and parse_object(f, "utf-8", objtype) 302 303 # If found, compare SEQUENCE and potentially DTSTAMP. 304 305 if event: 306 sequence = get_value(event, "SEQUENCE") 307 dtstamp = get_value(event, "DTSTAMP") 308 309 # If the request refers to an older version of the event, ignore 310 # it. 311 312 old_dtstamp = self.dtstamp < dtstamp 313 314 if sequence is not None and ( 315 int(self.sequence) < int(sequence) or 316 int(self.sequence) == int(sequence) and old_dtstamp 317 ) or old_dtstamp: 318 319 return False 320 321 return True 322 323 # Handler registry. 324 325 methods = { 326 "ADD" : lambda handler: handler.add, 327 "CANCEL" : lambda handler: handler.cancel, 328 "COUNTER" : lambda handler: handler.counter, 329 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 330 "PUBLISH" : lambda handler: handler.publish, 331 "REFRESH" : lambda handler: handler.refresh, 332 "REPLY" : lambda handler: handler.reply, 333 "REQUEST" : lambda handler: handler.request, 334 } 335 336 # vim: tabstop=4 expandtab shiftwidth=4