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 parse_object(f, encoding, objtype=None): 90 91 """ 92 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 93 given, only objects of that type will be returned. Otherwise, the root of 94 the content will be returned as a dictionary with a single key indicating 95 the object type. 96 97 Return None if the content was not readable or suitable. 98 """ 99 100 try: 101 try: 102 doctype, attrs, elements = obj = parse(f, encoding=encoding) 103 if objtype and doctype == objtype: 104 return to_dict(obj)[objtype][0] 105 elif not objtype: 106 return to_dict(obj) 107 finally: 108 f.close() 109 110 # NOTE: Handle parse errors properly. 111 112 except (ParseError, ValueError): 113 pass 114 115 return None 116 117 def to_part(method, calendar): 118 119 """ 120 Write using the given 'method', the 'calendar' details to a MIME 121 text/calendar part. 122 """ 123 124 encoding = "utf-8" 125 out = StringIO() 126 try: 127 to_stream(out, make_calendar(calendar, method), encoding) 128 part = MIMEText(out.getvalue(), "calendar", encoding) 129 part.set_param("method", method) 130 return part 131 132 finally: 133 out.close() 134 135 def to_stream(out, fragment, encoding="utf-8"): 136 iterwrite(out, encoding=encoding).append(fragment) 137 138 # Structure access functions. 139 140 def get_fragments(d, name): 141 142 """ 143 Return all fragments from 'd' with the given 'name'. Each fragment is thus 144 suitable for using to initialise Object instances. 145 """ 146 147 fragments = [] 148 if d.has_key(name): 149 for value in d[name]: 150 fragments.append(dict([(name, value)])) 151 return fragments 152 153 def get_items(d, name, all=True): 154 155 """ 156 Get all items from 'd' for the given 'name', returning single items if 157 'all' is specified and set to a false value and if only one value is 158 present for the name. Return None if no items are found for the name or if 159 many items are found but 'all' is set to a false value. 160 """ 161 162 if d.has_key(name): 163 values = d[name] 164 if all: 165 return values 166 elif len(values) == 1: 167 return values[0] 168 else: 169 return None 170 else: 171 return None 172 173 def get_item(d, name): 174 return get_items(d, name, False) 175 176 def get_value_map(d, name): 177 178 """ 179 Return a dictionary for all items in 'd' having the given 'name'. The 180 dictionary will map values for the name to any attributes or qualifiers 181 that may have been present. 182 """ 183 184 items = get_items(d, name) 185 if items: 186 return dict(items) 187 else: 188 return {} 189 190 def get_values(d, name, all=True): 191 if d.has_key(name): 192 values = d[name] 193 if not all and len(values) == 1: 194 return values[0][0] 195 else: 196 return map(lambda x: x[0], values) 197 else: 198 return None 199 200 def get_value(d, name): 201 return get_values(d, name, False) 202 203 def get_utc_datetime(d, name): 204 value, attr = get_item(d, name) 205 dt = get_datetime(value, attr) 206 return to_utc_datetime(dt) 207 208 def get_addresses(values): 209 return [address for name, address in email.utils.getaddresses(values)] 210 211 def get_address(value): 212 return value.lower().startswith("mailto:") and value.lower()[7:] or value 213 214 def get_uri(value): 215 return value.lower().startswith("mailto:") and value.lower() or ":" in value and value or "mailto:%s" % value.lower() 216 217 def uri_dict(d): 218 return dict([(get_uri(key), value) for key, value in d.items()]) 219 220 def uri_item(item): 221 return get_uri(item[0]), item[1] 222 223 def uri_items(items): 224 return [(get_uri(value), attr) for value, attr in items] 225 226 # Operations on structure data. 227 228 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, partstat_set): 229 230 """ 231 Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and 232 'new_dtstamp', and the 'partstat_set' indication, whether the object 233 providing the new information is really newer than the object providing the 234 old information. 235 """ 236 237 have_sequence = old_sequence is not None and new_sequence is not None 238 is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence) 239 240 have_dtstamp = old_dtstamp and new_dtstamp 241 is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp 242 243 is_old_sequence = have_sequence and ( 244 int(new_sequence) < int(old_sequence) or 245 is_same_sequence and is_old_dtstamp 246 ) 247 248 return is_same_sequence and partstat_set or not is_old_sequence 249 250 # vim: tabstop=4 expandtab shiftwidth=4