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 email.mime.text import MIMEText 23 from imiptools.dates import get_datetime, to_utc_datetime 24 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node 25 import email.utils 26 27 try: 28 from cStringIO import StringIO 29 except ImportError: 30 from StringIO import StringIO 31 32 class Object: 33 34 "Access to calendar structures." 35 36 def __init__(self, fragment): 37 self.objtype, (self.details, self.attr) = fragment.items()[0] 38 39 def get_items(self, name, all=True): 40 return get_items(self.details, name, all) 41 42 def get_item(self, name): 43 return get_item(self.details, name) 44 45 def get_value_map(self, name): 46 return get_value_map(self.details, name) 47 48 def get_values(self, name, all=True): 49 return get_values(self.details, name, all) 50 51 def get_value(self, name): 52 return get_value(self.details, name) 53 54 def get_utc_datetime(self, name): 55 return get_utc_datetime(self.details, name) 56 57 def to_node(self): 58 return to_node({self.objtype : [(self.details, self.attr)]}) 59 60 def to_part(self, method): 61 return to_part(method, [self.to_node()]) 62 63 # Direct access to the structure. 64 65 def __getitem__(self, name): 66 return self.details[name] 67 68 def __setitem__(self, name, value): 69 self.details[name] = value 70 71 def __delitem__(self, name): 72 del self.details[name] 73 74 # Construction and serialisation. 75 76 def make_calendar(nodes, method=None): 77 78 """ 79 Return a complete calendar node wrapping the given 'nodes' and employing the 80 given 'method', if indicated. 81 """ 82 83 return ("VCALENDAR", {}, 84 (method and [("METHOD", {}, method)] or []) + 85 [("VERSION", {}, "2.0")] + 86 nodes 87 ) 88 89 def make_freebusy(freebusy, uid, organiser, attendee=None): 90 91 """ 92 Return a calendar node defining the free/busy details described in the given 93 'freebusy' list, employing the given 'uid', for the given 'organiser', with 94 the optional 'attendee' providing recipient details. 95 """ 96 97 record = [] 98 rwrite = record.append 99 100 rwrite(("ORGANIZER", {}, organiser)) 101 102 if attendee: 103 rwrite(("ATTENDEE", {}, attendee)) 104 105 rwrite(("UID", {}, uid)) 106 107 if freebusy: 108 for start, end, uid, transp in freebusy: 109 if transp == "OPAQUE": 110 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end]))) 111 112 return ("VFREEBUSY", {}, record) 113 114 def parse_object(f, encoding, objtype=None): 115 116 """ 117 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 118 given, only objects of that type will be returned. Otherwise, the root of 119 the content will be returned as a dictionary with a single key indicating 120 the object type. 121 122 Return None if the content was not readable or suitable. 123 """ 124 125 try: 126 try: 127 doctype, attrs, elements = obj = parse(f, encoding=encoding) 128 if objtype and doctype == objtype: 129 return to_dict(obj)[objtype][0] 130 elif not objtype: 131 return to_dict(obj) 132 finally: 133 f.close() 134 135 # NOTE: Handle parse errors properly. 136 137 except (ParseError, ValueError): 138 pass 139 140 return None 141 142 def to_part(method, calendar): 143 144 """ 145 Write using the given 'method', the 'calendar' details to a MIME 146 text/calendar part. 147 """ 148 149 encoding = "utf-8" 150 out = StringIO() 151 try: 152 to_stream(out, make_calendar(calendar, method), encoding) 153 part = MIMEText(out.getvalue(), "calendar", encoding) 154 part.set_param("method", method) 155 return part 156 157 finally: 158 out.close() 159 160 def to_stream(out, fragment, encoding="utf-8"): 161 iterwrite(out, encoding=encoding).append(fragment) 162 163 # Structure access functions. 164 165 def get_fragments(d, name): 166 167 """ 168 Return all fragments from 'd' with the given 'name'. Each fragment is thus 169 suitable for using to initialise Object instances. 170 """ 171 172 fragments = [] 173 if d.has_key(name): 174 for value in d[name]: 175 fragments.append(dict([(name, value)])) 176 return fragments 177 178 def get_items(d, name, all=True): 179 180 """ 181 Get all items from 'd' for the given 'name', returning single items if 182 'all' is specified and set to a false value and if only one value is 183 present for the name. Return None if no items are found for the name or if 184 many items are found but 'all' is set to a false value. 185 """ 186 187 if d.has_key(name): 188 values = d[name] 189 if all: 190 return values 191 elif len(values) == 1: 192 return values[0] 193 else: 194 return None 195 else: 196 return None 197 198 def get_item(d, name): 199 return get_items(d, name, False) 200 201 def get_value_map(d, name): 202 203 """ 204 Return a dictionary for all items in 'd' having the given 'name'. The 205 dictionary will map values for the name to any attributes or qualifiers 206 that may have been present. 207 """ 208 209 items = get_items(d, name) 210 if items: 211 return dict(items) 212 else: 213 return {} 214 215 def get_values(d, name, all=True): 216 if d.has_key(name): 217 values = d[name] 218 if not all and len(values) == 1: 219 return values[0][0] 220 else: 221 return map(lambda x: x[0], values) 222 else: 223 return None 224 225 def get_value(d, name): 226 return get_values(d, name, False) 227 228 def get_utc_datetime(d, name): 229 value, attr = get_item(d, name) 230 dt = get_datetime(value, attr) 231 return to_utc_datetime(dt) 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 # vim: tabstop=4 expandtab shiftwidth=4