1 #!/usr/bin/env python 2 3 """ 4 Interpretation of vCalendar content. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import datetime, timedelta 23 from email.mime.text import MIMEText 24 from imiptools.dates import format_datetime, get_datetime, to_utc_datetime 25 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node 26 from vRecurrence import get_parameters, get_rule 27 import email.utils 28 29 try: 30 from cStringIO import StringIO 31 except ImportError: 32 from StringIO import StringIO 33 34 class Object: 35 36 "Access to calendar structures." 37 38 def __init__(self, fragment): 39 self.objtype, (self.details, self.attr) = fragment.items()[0] 40 41 def get_items(self, name, all=True): 42 return get_items(self.details, name, all) 43 44 def get_item(self, name): 45 return get_item(self.details, name) 46 47 def get_value_map(self, name): 48 return get_value_map(self.details, name) 49 50 def get_values(self, name, all=True): 51 return get_values(self.details, name, all) 52 53 def get_value(self, name): 54 return get_value(self.details, name) 55 56 def get_utc_datetime(self, name): 57 return get_utc_datetime(self.details, name) 58 59 def get_datetime_item(self, name): 60 return get_datetime_item(self.details, name) 61 62 def to_node(self): 63 return to_node({self.objtype : [(self.details, self.attr)]}) 64 65 def to_part(self, method): 66 return to_part(method, [self.to_node()]) 67 68 # Direct access to the structure. 69 70 def __getitem__(self, name): 71 return self.details[name] 72 73 def __setitem__(self, name, value): 74 self.details[name] = value 75 76 def __delitem__(self, name): 77 del self.details[name] 78 79 # Computed results. 80 81 def get_periods(self, window_size=100): 82 return get_periods(self, window_size) 83 84 # Construction and serialisation. 85 86 def make_calendar(nodes, method=None): 87 88 """ 89 Return a complete calendar node wrapping the given 'nodes' and employing the 90 given 'method', if indicated. 91 """ 92 93 return ("VCALENDAR", {}, 94 (method and [("METHOD", {}, method)] or []) + 95 [("VERSION", {}, "2.0")] + 96 nodes 97 ) 98 99 def make_freebusy(freebusy, uid, organiser, attendee=None): 100 101 """ 102 Return a calendar node defining the free/busy details described in the given 103 'freebusy' list, employing the given 'uid', for the given 'organiser', with 104 the optional 'attendee' providing recipient details. 105 """ 106 107 record = [] 108 rwrite = record.append 109 110 rwrite(("ORGANIZER", {}, organiser)) 111 112 if attendee: 113 rwrite(("ATTENDEE", {}, attendee)) 114 115 rwrite(("UID", {}, uid)) 116 117 if freebusy: 118 for start, end, uid, transp in freebusy: 119 if transp == "OPAQUE": 120 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 121 122 return ("VFREEBUSY", {}, record) 123 124 def parse_object(f, encoding, objtype=None): 125 126 """ 127 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 128 given, only objects of that type will be returned. Otherwise, the root of 129 the content will be returned as a dictionary with a single key indicating 130 the object type. 131 132 Return None if the content was not readable or suitable. 133 """ 134 135 try: 136 try: 137 doctype, attrs, elements = obj = parse(f, encoding=encoding) 138 if objtype and doctype == objtype: 139 return to_dict(obj)[objtype][0] 140 elif not objtype: 141 return to_dict(obj) 142 finally: 143 f.close() 144 145 # NOTE: Handle parse errors properly. 146 147 except (ParseError, ValueError): 148 pass 149 150 return None 151 152 def to_part(method, calendar): 153 154 """ 155 Write using the given 'method', the 'calendar' details to a MIME 156 text/calendar part. 157 """ 158 159 encoding = "utf-8" 160 out = StringIO() 161 try: 162 to_stream(out, make_calendar(calendar, method), encoding) 163 part = MIMEText(out.getvalue(), "calendar", encoding) 164 part.set_param("method", method) 165 return part 166 167 finally: 168 out.close() 169 170 def to_stream(out, fragment, encoding="utf-8"): 171 iterwrite(out, encoding=encoding).append(fragment) 172 173 # Structure access functions. 174 175 def get_items(d, name, all=True): 176 177 """ 178 Get all items from 'd' for the given 'name', returning single items if 179 'all' is specified and set to a false value and if only one value is 180 present for the name. Return None if no items are found for the name or if 181 many items are found but 'all' is set to a false value. 182 """ 183 184 if d.has_key(name): 185 values = d[name] 186 if all: 187 return values 188 elif len(values) == 1: 189 return values[0] 190 else: 191 return None 192 else: 193 return None 194 195 def get_item(d, name): 196 return get_items(d, name, False) 197 198 def get_value_map(d, name): 199 200 """ 201 Return a dictionary for all items in 'd' having the given 'name'. The 202 dictionary will map values for the name to any attributes or qualifiers 203 that may have been present. 204 """ 205 206 items = get_items(d, name) 207 if items: 208 return dict(items) 209 else: 210 return {} 211 212 def get_values(d, name, all=True): 213 if d.has_key(name): 214 values = d[name] 215 if not all and len(values) == 1: 216 return values[0][0] 217 else: 218 return map(lambda x: x[0], values) 219 else: 220 return None 221 222 def get_value(d, name): 223 return get_values(d, name, False) 224 225 def get_utc_datetime(d, name): 226 dt, attr = get_datetime_item(d, name) 227 return to_utc_datetime(dt) 228 229 def get_datetime_item(d, name): 230 value, attr = get_item(d, name) 231 return get_datetime(value, attr), attr 232 233 def get_addresses(values): 234 return [address for name, address in email.utils.getaddresses(values)] 235 236 def get_address(value): 237 return value.lower().startswith("mailto:") and value.lower()[7:] or value 238 239 def get_uri(value): 240 return value.lower().startswith("mailto:") and value.lower() or ":" in value and value or "mailto:%s" % value.lower() 241 242 def uri_dict(d): 243 return dict([(get_uri(key), value) for key, value in d.items()]) 244 245 def uri_item(item): 246 return get_uri(item[0]), item[1] 247 248 def uri_items(items): 249 return [(get_uri(value), attr) for value, attr in items] 250 251 # Operations on structure data. 252 253 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, partstat_set): 254 255 """ 256 Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and 257 'new_dtstamp', and the 'partstat_set' indication, whether the object 258 providing the new information is really newer than the object providing the 259 old information. 260 """ 261 262 have_sequence = old_sequence is not None and new_sequence is not None 263 is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence) 264 265 have_dtstamp = old_dtstamp and new_dtstamp 266 is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp 267 268 is_old_sequence = have_sequence and ( 269 int(new_sequence) < int(old_sequence) or 270 is_same_sequence and is_old_dtstamp 271 ) 272 273 return is_same_sequence and partstat_set or not is_old_sequence 274 275 # NOTE: Need to expose the 100 day window for recurring events in the 276 # NOTE: configuration. 277 278 def get_periods(obj, window_size=100): 279 280 """ 281 Return periods for the given object 'obj', confining materialised periods 282 to the given 'window_size' in days starting from the present moment. 283 """ 284 285 dtstart = obj.get_utc_datetime("DTSTART") 286 dtend = obj.get_utc_datetime("DTEND") 287 288 # NOTE: Need also DURATION support. 289 290 duration = dtend - dtstart 291 292 # Recurrence rules create multiple instances to be checked. 293 # Conflicts may only be assessed within a period defined by policy 294 # for the agent, with instances outside that period being considered 295 # unchecked. 296 297 window_end = datetime.now() + timedelta(window_size) 298 299 # NOTE: Need also RDATE and EXDATE support. 300 301 rrule = obj.get_value("RRULE") 302 303 if rrule: 304 selector = get_rule(dtstart, rrule) 305 parameters = get_parameters(rrule) 306 periods = [] 307 for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")): 308 start = datetime(*start, tzinfo=timezone("UTC")) 309 end = start + duration 310 periods.append((format_datetime(start), format_datetime(end))) 311 else: 312 periods = [(format_datetime(dtstart), format_datetime(dtend))] 313 314 return periods 315 316 # vim: tabstop=4 expandtab shiftwidth=4