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 to_timezone(dt, name): 95 try: 96 tz = name and timezone(name) or None 97 except UnknownTimeZoneError: 98 tz = None 99 if tz is not None: 100 if not dt.tzinfo: 101 return tz.localize(dt) 102 else: 103 return dt.astimezone(tz) 104 else: 105 return dt 106 107 def format_datetime(dt): 108 if not dt: 109 return None 110 elif isinstance(dt, datetime): 111 if dt.tzname() == "UTC": 112 return dt.strftime("%Y%m%dT%H%M%SZ") 113 else: 114 return dt.strftime("%Y%m%dT%H%M%S") 115 else: 116 return dt.strftime("%Y%m%d") 117 118 def get_address(value): 119 return value.startswith("mailto:") and value[7:] or value 120 121 def get_uri(value): 122 return value.startswith("mailto:") and value or "mailto:%s" % value 123 124 def get_datetime(value, attr=None): 125 126 """ 127 Return a datetime object from the given 'value' in iCalendar format, using 128 the 'attr' mapping (if specified) to control the conversion. 129 """ 130 131 if not attr or attr.get("VALUE") in (None, "DATE-TIME"): 132 m = match_datetime_icalendar(value) 133 if m: 134 dt = datetime( 135 int(m.group("year")), int(m.group("month")), int(m.group("day")), 136 int(m.group("hour")), int(m.group("minute")), int(m.group("second")) 137 ) 138 139 # Impose the indicated timezone. 140 # NOTE: This needs an ambiguity policy for DST changes. 141 142 return to_timezone(dt, m.group("utc") and "UTC" or attr and attr.get("TZID") or None) 143 144 if not attr or attr.get("VALUE") == "DATE": 145 m = match_date_icalendar(value) 146 if m: 147 return date( 148 int(m.group("year")), int(m.group("month")), int(m.group("day")) 149 ) 150 return None 151 152 # Handler mechanism objects. 153 154 def handle_itip_part(part, recipients, handlers): 155 156 """ 157 Handle the given iTIP 'part' for the given 'recipients' using the given 158 'handlers'. Return a list of responses, each response being a tuple of the 159 form (is-outgoing, message-part). 160 """ 161 162 method = part.get_param("method") 163 164 # Decode the data and parse it. 165 166 f = StringIO(part.get_payload(decode=True)) 167 168 itip = parse_object(f, part.get_content_charset(), "VCALENDAR") 169 170 # Ignore the part if not a calendar object. 171 172 if not itip: 173 return [] 174 175 # Require consistency between declared and employed methods. 176 177 if get_value(itip, "METHOD") == method: 178 179 # Look for different kinds of sections. 180 181 all_results = [] 182 183 for name, cls in handlers: 184 for details in get_values(itip, name) or []: 185 186 # Dispatch to a handler and obtain any response. 187 188 handler = cls(details, recipients) 189 result = methods[method](handler)() 190 191 # Concatenate responses for a single calendar object. 192 193 if result: 194 response_method, part = result 195 outgoing = method != response_method 196 all_results.append((outgoing, part)) 197 198 return all_results 199 200 return [] 201 202 def parse_object(f, encoding, objtype=None): 203 204 """ 205 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 206 given, only objects of that type will be returned. 207 208 Return None if the content was not readable or suitable. 209 """ 210 211 try: 212 try: 213 doctype, attrs, elements = obj = parse(f, encoding=encoding) 214 if objtype and doctype == objtype: 215 return to_dict(obj)[objtype][0] 216 elif not objtype: 217 return to_dict(obj)[doctype][0] 218 finally: 219 f.close() 220 except (ParseError, ValueError): 221 pass 222 223 return None 224 225 def to_part(method, calendar): 226 227 """ 228 Write using the given 'method', the 'calendar' details to a MIME 229 text/calendar part. 230 """ 231 232 encoding = "utf-8" 233 out = StringIO() 234 try: 235 imip_store.to_stream(out, imip_store.make_calendar(calendar, method), encoding) 236 part = MIMEText(out.getvalue(), "calendar", encoding) 237 part.set_param("method", method) 238 return part 239 240 finally: 241 out.close() 242 243 class Handler: 244 245 "General handler support." 246 247 def __init__(self, details, recipients): 248 249 """ 250 Initialise the handler with the 'details' of a calendar object and the 251 'recipients' of the object. 252 """ 253 254 self.details = details 255 self.recipients = set(recipients) 256 257 self.uid = get_value(details, "UID") 258 self.sequence = get_value(details, "SEQUENCE") 259 self.dtstamp = get_value(details, "DTSTAMP") 260 261 self.store = imip_store.FileStore() 262 263 try: 264 self.publisher = imip_store.FilePublisher() 265 except OSError: 266 self.publisher = None 267 268 # Access to calendar structures and other data. 269 270 def get_items(self, name, all=True): 271 return get_items(self.details, name, all) 272 273 def get_item(self, name): 274 return get_item(self.details, name) 275 276 def get_value_map(self, name): 277 return get_value_map(self.details, name) 278 279 def get_values(self, name, all=True): 280 return get_values(self.details, name, all) 281 282 def get_value(self, name): 283 return get_value(self.details, name) 284 285 def get_utc_datetime(self, name): 286 return get_utc_datetime(self.details, name) 287 288 def filter_by_recipients(self, values): 289 return self.recipients.intersection(map(get_address, values)) 290 291 def require_organiser_and_attendees(self): 292 293 """ 294 Return the organiser and attendees for the current object, filtered by 295 the recipients of interest. Return None if no identities are eligible. 296 """ 297 298 attendee_map = self.get_value_map("ATTENDEE") 299 organiser = self.get_item("ORGANIZER") 300 301 # Only provide details for recipients who are also attendees. 302 303 attendees = {} 304 for attendee in map(get_uri, self.filter_by_recipients(attendee_map)): 305 attendees[attendee] = attendee_map[attendee] 306 307 if not attendees and not organiser: 308 return None 309 310 return organiser, attendees 311 312 def have_new_object(self, attendee, objtype): 313 314 """ 315 Return whether the current object is new to the 'attendee' for the 316 given 'objtype'. 317 """ 318 319 f = self.store.get_event(attendee, self.uid) 320 event = f and parse_object(f, "utf-8", objtype) 321 322 # If found, compare SEQUENCE and potentially DTSTAMP. 323 324 if event: 325 sequence = get_value(event, "SEQUENCE") 326 dtstamp = get_value(event, "DTSTAMP") 327 328 # If the request refers to an older version of the event, ignore 329 # it. 330 331 old_dtstamp = self.dtstamp < dtstamp 332 333 if sequence is not None and ( 334 int(self.sequence) < int(sequence) or 335 int(self.sequence) == int(sequence) and old_dtstamp 336 ) or old_dtstamp: 337 338 return False 339 340 return True 341 342 # Handler registry. 343 344 methods = { 345 "ADD" : lambda handler: handler.add, 346 "CANCEL" : lambda handler: handler.cancel, 347 "COUNTER" : lambda handler: handler.counter, 348 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 349 "PUBLISH" : lambda handler: handler.publish, 350 "REFRESH" : lambda handler: handler.refresh, 351 "REPLY" : lambda handler: handler.reply, 352 "REQUEST" : lambda handler: handler.request, 353 } 354 355 # vim: tabstop=4 expandtab shiftwidth=4