1 #!/usr/bin/env python 2 3 """ 4 Interpretation of vCalendar content. 5 6 Copyright (C) 2014, 2015, 2016, 2017 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 bisect import bisect_left 23 from datetime import date, datetime, timedelta 24 from email.mime.text import MIMEText 25 from imiptools.dates import format_datetime, get_datetime, \ 26 get_datetime_item as get_item_from_datetime, \ 27 get_datetime_tzid, \ 28 get_duration, get_period, get_period_item, \ 29 get_recurrence_start_point, \ 30 get_time, get_timestamp, get_tzid, to_datetime, \ 31 to_timezone, to_utc_datetime 32 from imiptools.freebusy import FreeBusyPeriod 33 from imiptools.period import Period, RecurringPeriod 34 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node 35 from vRecurrence import get_parameters, get_rule 36 import email.utils 37 38 try: 39 from cStringIO import StringIO 40 except ImportError: 41 from StringIO import StringIO 42 43 class Object: 44 45 "Access to calendar structures." 46 47 def __init__(self, fragment): 48 49 """ 50 Initialise the object with the given 'fragment'. This must be a 51 dictionary mapping an object type (such as "VEVENT") to a tuple 52 containing the object details and attributes, each being a dictionary 53 itself. 54 55 The result of parse_object can be processed to obtain a fragment by 56 obtaining a collection of records for an object type. For example: 57 58 l = parse_object(f, encoding, "VCALENDAR") 59 events = l["VEVENT"] 60 event = events[0] 61 62 Then, the specific object must be presented as follows: 63 64 object = Object({"VEVENT" : event}) 65 66 A convienience function is also provided to initialise objects: 67 68 object = new_object("VEVENT") 69 """ 70 71 self.objtype, (self.details, self.attr) = fragment.items()[0] 72 73 def get_uid(self): 74 return self.get_value("UID") 75 76 def get_recurrenceid(self): 77 78 """ 79 Return the recurrence identifier, normalised to a UTC datetime if 80 specified as a datetime or date with accompanying time zone information, 81 maintained as a date or floating datetime otherwise. If no recurrence 82 identifier is present, None is returned. 83 84 Note that this normalised form of the identifier may well not be the 85 same as the originally-specified identifier because that could have been 86 specified using an accompanying TZID attribute, whereas the normalised 87 form is effectively a converted datetime value. 88 """ 89 90 if not self.has_key("RECURRENCE-ID"): 91 return None 92 dt, attr = self.get_datetime_item("RECURRENCE-ID") 93 94 # Coerce any date to a UTC datetime if TZID was specified. 95 96 tzid = attr.get("TZID") 97 if tzid: 98 dt = to_timezone(to_datetime(dt, tzid), "UTC") 99 return format_datetime(dt) 100 101 def get_recurrence_start_point(self, recurrenceid, tzid): 102 103 """ 104 Return the start point corresponding to the given 'recurrenceid', using 105 the fallback 'tzid' to define the specific point in time referenced by 106 the recurrence identifier if the identifier has a date representation. 107 108 If 'recurrenceid' is given as None, this object's recurrence identifier 109 is used to obtain a start point, but if this object does not provide a 110 recurrence, None is returned. 111 112 A start point is typically used to match free/busy periods which are 113 themselves defined in terms of UTC datetimes. 114 """ 115 116 recurrenceid = recurrenceid or self.get_recurrenceid() 117 if recurrenceid: 118 return get_recurrence_start_point(recurrenceid, tzid) 119 else: 120 return None 121 122 def get_recurrence_start_points(self, recurrenceids, tzid): 123 return [self.get_recurrence_start_point(recurrenceid, tzid) for recurrenceid in recurrenceids] 124 125 # Structure access. 126 127 def add(self, obj): 128 129 "Add 'obj' to the structure." 130 131 name = obj.objtype 132 if not self.details.has_key(name): 133 l = self.details[name] = [] 134 else: 135 l = self.details[name] 136 l.append((obj.details, obj.attr)) 137 138 def copy(self): 139 return Object(self.to_dict()) 140 141 # Access to (value, attributes) items. 142 143 def get_items(self, name, all=True): 144 return get_items(self.details, name, all) 145 146 def get_item(self, name): 147 return get_item(self.details, name) 148 149 # Access to mappings. 150 151 def get_value_map(self, name): 152 return get_value_map(self.details, name) 153 154 # Access to mapped values. 155 156 def get_values(self, name, all=True): 157 return get_values(self.details, name, all) 158 159 def get_value(self, name): 160 return get_value(self.details, name) 161 162 # Convenience methods asserting URI values. 163 164 def get_uri_items(self, name, all=True): 165 return uri_items(self.get_items(name, all)) 166 167 def get_uri_item(self, name): 168 return uri_item(self.get_item(name)) 169 170 def get_uri_map(self, name): 171 return uri_dict(self.get_value_map(name)) 172 173 def get_uri_values(self, name): 174 return uri_values(self.get_values(name)) 175 176 def get_uri_value(self, name): 177 return uri_value(self.get_value(name)) 178 179 get_uri = get_uri_value 180 181 # Access to details as temporal objects. 182 183 def get_utc_datetime(self, name, date_tzid=None): 184 return get_utc_datetime(self.details, name, date_tzid) 185 186 def get_date_value_items(self, name, tzid=None): 187 return get_date_value_items(self.details, name, tzid) 188 189 def get_date_value_item_periods(self, name, tzid=None): 190 return get_date_value_item_periods(self.details, name, self.get_main_period(tzid).get_duration(), tzid) 191 192 def get_period_values(self, name, tzid=None): 193 return get_period_values(self.details, name, tzid) 194 195 def get_datetime(self, name): 196 t = get_datetime_item(self.details, name) 197 if not t: return None 198 dt, attr = t 199 return dt 200 201 def get_datetime_item(self, name): 202 return get_datetime_item(self.details, name) 203 204 def get_duration(self, name): 205 return get_duration(self.get_value(name)) 206 207 # Serialisation. 208 209 def to_dict(self): 210 return to_dict(self.to_node()) 211 212 def to_node(self): 213 return to_node({self.objtype : [(self.details, self.attr)]}) 214 215 def to_part(self, method, encoding="utf-8", line_length=None): 216 return to_part(method, [self.to_node()], encoding, line_length) 217 218 def to_string(self, encoding="utf-8", line_length=None): 219 return to_string(self.to_node(), encoding, line_length) 220 221 # Direct access to the structure. 222 223 def has_key(self, name): 224 return self.details.has_key(name) 225 226 def get(self, name): 227 return self.details.get(name) 228 229 def keys(self): 230 return self.details.keys() 231 232 def __getitem__(self, name): 233 return self.details[name] 234 235 def __setitem__(self, name, value): 236 self.details[name] = value 237 238 def __delitem__(self, name): 239 del self.details[name] 240 241 def remove(self, name): 242 try: 243 del self[name] 244 except KeyError: 245 pass 246 247 def remove_all(self, names): 248 for name in names: 249 self.remove(name) 250 251 def preserve(self, names): 252 for name in self.keys(): 253 if not name in names: 254 self.remove(name) 255 256 # Computed results. 257 258 def get_main_period(self, tzid=None): 259 260 """ 261 Return a period object corresponding to the main start-end period for 262 the object. 263 """ 264 265 (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items() 266 tzid = tzid or get_tzid(dtstart_attr, dtend_attr) 267 return RecurringPeriod(dtstart, dtend, tzid, "DTSTART", dtstart_attr, dtend_attr) 268 269 def get_main_period_items(self): 270 271 """ 272 Return two (value, attributes) items corresponding to the main start-end 273 period for the object. 274 """ 275 276 dtstart, dtstart_attr = self.get_datetime_item("DTSTART") 277 278 if self.has_key("DTEND"): 279 dtend, dtend_attr = self.get_datetime_item("DTEND") 280 elif self.has_key("DURATION"): 281 duration = self.get_duration("DURATION") 282 dtend = dtstart + duration 283 dtend_attr = dtstart_attr 284 else: 285 dtend, dtend_attr = dtstart, dtstart_attr 286 287 return (dtstart, dtstart_attr), (dtend, dtend_attr) 288 289 def get_periods(self, tzid, start=None, end=None, inclusive=False): 290 291 """ 292 Return periods defined by this object, employing the given 'tzid' where 293 no time zone information is defined, and limiting the collection to a 294 window of time with the given 'start' and 'end'. 295 296 If 'end' is omitted, only explicit recurrences and recurrences from 297 explicitly-terminated rules will be returned. 298 299 If 'inclusive' is set to a true value, any period occurring at the 'end' 300 will be included. 301 """ 302 303 return get_periods(self, tzid, start, end, inclusive) 304 305 def has_period(self, tzid, period): 306 307 """ 308 Return whether this object, employing the given 'tzid' where no time 309 zone information is defined, has the given 'period'. 310 """ 311 312 return period in self.get_periods(tzid, end=period.get_start_point(), inclusive=True) 313 314 def has_recurrence(self, tzid, recurrenceid): 315 316 """ 317 Return whether this object, employing the given 'tzid' where no time 318 zone information is defined, has the given 'recurrenceid'. 319 """ 320 321 start_point = self.get_recurrence_start_point(recurrenceid, tzid) 322 for p in self.get_periods(tzid, end=start_point, inclusive=True): 323 if p.get_start_point() == start_point: 324 return True 325 return False 326 327 def get_active_periods(self, recurrenceids, tzid, start=None, end=None): 328 329 """ 330 Return all periods specified by this object that are not replaced by 331 those defined by 'recurrenceids', using 'tzid' as a fallback time zone 332 to convert floating dates and datetimes, and using 'start' and 'end' to 333 respectively indicate the start and end of the time window within which 334 periods are considered. 335 """ 336 337 # Specific recurrences yield all specified periods. 338 339 periods = self.get_periods(tzid, start, end) 340 341 if self.get_recurrenceid(): 342 return periods 343 344 # Parent objects need to have their periods tested against redefined 345 # recurrences. 346 347 active = [] 348 349 for p in periods: 350 351 # Subtract any recurrences from the free/busy details of a 352 # parent object. 353 354 if not p.is_replaced(recurrenceids): 355 active.append(p) 356 357 return active 358 359 def get_freebusy_period(self, period, only_organiser=False): 360 361 """ 362 Return a free/busy period for the given 'period' provided by this 363 object, using the 'only_organiser' status to produce a suitable 364 transparency value. 365 """ 366 367 return FreeBusyPeriod( 368 period.get_start_point(), 369 period.get_end_point(), 370 self.get_value("UID"), 371 only_organiser and "ORG" or self.get_value("TRANSP") or "OPAQUE", 372 self.get_recurrenceid(), 373 self.get_value("SUMMARY"), 374 self.get_uri("ORGANIZER") 375 ) 376 377 def get_participation_status(self, participant): 378 379 """ 380 Return the participation status of the given 'participant', with the 381 special value "ORG" indicating organiser-only participation. 382 """ 383 384 attendees = self.get_uri_map("ATTENDEE") 385 organiser = self.get_uri("ORGANIZER") 386 387 attendee_attr = attendees.get(participant) 388 if attendee_attr: 389 return attendee_attr.get("PARTSTAT", "NEEDS-ACTION") 390 elif organiser == participant: 391 return "ORG" 392 393 return None 394 395 def get_participation(self, partstat, include_needs_action=False): 396 397 """ 398 Return whether 'partstat' indicates some kind of participation in an 399 event. If 'include_needs_action' is specified as a true value, events 400 not yet responded to will be treated as events with tentative 401 participation. 402 """ 403 404 return not partstat in ("DECLINED", "DELEGATED", "NEEDS-ACTION") or \ 405 include_needs_action and partstat == "NEEDS-ACTION" or \ 406 partstat == "ORG" 407 408 def get_tzid(self): 409 410 """ 411 Return a time zone identifier used by the start or end datetimes, 412 potentially suitable for converting dates to datetimes. 413 """ 414 415 if not self.has_key("DTSTART"): 416 return None 417 dtstart, dtstart_attr = self.get_datetime_item("DTSTART") 418 if self.has_key("DTEND"): 419 dtend, dtend_attr = self.get_datetime_item("DTEND") 420 else: 421 dtend_attr = None 422 return get_tzid(dtstart_attr, dtend_attr) 423 424 def is_shared(self): 425 426 """ 427 Return whether this object is shared based on the presence of a SEQUENCE 428 property. 429 """ 430 431 return self.get_value("SEQUENCE") is not None 432 433 def possibly_active_from(self, dt, tzid): 434 435 """ 436 Return whether the object is possibly active from or after the given 437 datetime 'dt' using 'tzid' to convert any dates or floating datetimes. 438 """ 439 440 dt = to_datetime(dt, tzid) 441 periods = self.get_periods(tzid) 442 443 for p in periods: 444 if p.get_end_point() > dt: 445 return True 446 447 return self.possibly_recurring_indefinitely() 448 449 def possibly_recurring_indefinitely(self): 450 451 "Return whether this object may recur indefinitely." 452 453 rrule = self.get_value("RRULE") 454 parameters = rrule and get_parameters(rrule) 455 until = parameters and parameters.get("UNTIL") 456 count = parameters and parameters.get("COUNT") 457 458 # Non-recurring periods or constrained recurrences. 459 460 if not rrule or until or count: 461 return False 462 463 # Unconstrained recurring periods will always lie beyond any specified 464 # datetime. 465 466 else: 467 return True 468 469 # Modification methods. 470 471 def set_datetime(self, name, dt, tzid=None): 472 473 """ 474 Set a datetime for property 'name' using 'dt' and the optional fallback 475 'tzid', returning whether an update has occurred. 476 """ 477 478 if dt: 479 old_value = self.get_value(name) 480 self[name] = [get_item_from_datetime(dt, tzid)] 481 return format_datetime(dt) != old_value 482 483 return False 484 485 def set_period(self, period): 486 487 "Set the given 'period' as the main start and end." 488 489 result = self.set_datetime("DTSTART", period.get_start()) 490 result = self.set_datetime("DTEND", period.get_end()) or result 491 if self.has_key("DURATION"): 492 del self["DURATION"] 493 494 return result 495 496 def set_periods(self, periods): 497 498 """ 499 Set the given 'periods' as recurrence date properties, replacing the 500 previous RDATE properties and ignoring any RRULE properties. 501 """ 502 503 old_values = set(self.get_date_value_item_periods("RDATE") or []) 504 new_rdates = [] 505 506 if self.has_key("RDATE"): 507 del self["RDATE"] 508 509 main_changed = False 510 511 for p in periods: 512 if p.origin == "RDATE" and p != self.get_main_period(): 513 new_rdates.append(get_period_item(p.get_start(), p.get_end())) 514 elif p.origin == "DTSTART": 515 main_changed = self.set_period(p) 516 517 if new_rdates: 518 self["RDATE"] = new_rdates 519 520 return main_changed or old_values != set(self.get_date_value_item_periods("RDATE") or []) 521 522 def set_rule(self, rule): 523 524 """ 525 Set the given 'rule' in this object, replacing the previous RRULE 526 property, returning whether the object has changed. The provided 'rule' 527 must be an item. 528 """ 529 530 if not rule: 531 return False 532 533 old_rrule = self.get_item("RRULE") 534 self["RRULE"] = [rule] 535 return old_rrule != rule 536 537 def set_exceptions(self, exceptions): 538 539 """ 540 Set the given 'exceptions' in this object, replacing the previous EXDATE 541 properties, returning whether the object has changed. The provided 542 'exceptions' must be a collection of items. 543 """ 544 545 old_exdates = set(self.get_date_value_item_periods("EXDATE") or []) 546 if exceptions: 547 self["EXDATE"] = exceptions 548 return old_exdates != set(self.get_date_value_item_periods("EXDATE") or []) 549 elif old_exdates: 550 del self["EXDATE"] 551 return True 552 else: 553 return False 554 555 def update_dtstamp(self): 556 557 "Update the DTSTAMP in the object." 558 559 dtstamp = self.get_utc_datetime("DTSTAMP") 560 utcnow = get_time() 561 dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow) 562 self["DTSTAMP"] = [(dtstamp, {})] 563 return dtstamp 564 565 def update_sequence(self, increment=False): 566 567 "Set or update the SEQUENCE in the object." 568 569 sequence = self.get_value("SEQUENCE") or "0" 570 self["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})] 571 return sequence 572 573 def update_exceptions(self, excluded, asserted): 574 575 """ 576 Update the exceptions to any rule by applying the list of 'excluded' 577 periods. Where 'asserted' periods are provided, exceptions will be 578 removed corresponding to those periods. 579 """ 580 581 old_exdates = self.get_date_value_item_periods("EXDATE") or [] 582 new_exdates = set(old_exdates) 583 new_exdates.update(excluded) 584 new_exdates.difference_update(asserted) 585 586 if not new_exdates and self.has_key("EXDATE"): 587 del self["EXDATE"] 588 else: 589 self["EXDATE"] = [] 590 for p in new_exdates: 591 self["EXDATE"].append(get_period_item(p.get_start(), p.get_end())) 592 593 return set(old_exdates) != new_exdates 594 595 def correct_object(self, tzid, permitted_values): 596 597 """ 598 Correct the object's period details using the given 'tzid' and 599 'permitted_values'. 600 """ 601 602 corrected = set() 603 rdates = [] 604 605 for period in self.get_periods(tzid): 606 corrected_period = period.get_corrected(permitted_values) 607 608 if corrected_period is period: 609 if period.origin == "RDATE": 610 rdates.append(period) 611 continue 612 613 if period.origin == "DTSTART": 614 self.set_period(corrected_period) 615 corrected.add("DTSTART") 616 elif period.origin == "RDATE": 617 rdates.append(corrected_period) 618 corrected.add("RDATE") 619 620 if "RDATE" in corrected: 621 self.set_periods(rdates) 622 623 return corrected 624 625 # Construction and serialisation. 626 627 def make_calendar(nodes, method=None): 628 629 """ 630 Return a complete calendar node wrapping the given 'nodes' and employing the 631 given 'method', if indicated. 632 """ 633 634 return ("VCALENDAR", {}, 635 (method and [("METHOD", {}, method)] or []) + 636 [("VERSION", {}, "2.0")] + 637 nodes 638 ) 639 640 def make_freebusy(freebusy, uid, organiser, organiser_attr=None, attendee=None, 641 attendee_attr=None, period=None): 642 643 """ 644 Return a calendar node defining the free/busy details described in the given 645 'freebusy' list, employing the given 'uid', for the given 'organiser' and 646 optional 'organiser_attr', with the optional 'attendee' providing recipient 647 details together with the optional 'attendee_attr'. 648 649 The result will be constrained to the 'period' if specified. 650 """ 651 652 record = [] 653 rwrite = record.append 654 655 rwrite(("ORGANIZER", organiser_attr or {}, organiser)) 656 657 if attendee: 658 rwrite(("ATTENDEE", attendee_attr or {}, attendee)) 659 660 rwrite(("UID", {}, uid)) 661 662 if freebusy: 663 664 # Get a constrained view if start and end limits are specified. 665 666 if period: 667 periods = freebusy.get_overlapping([period]) 668 else: 669 periods = freebusy 670 671 # Write the limits of the resource. 672 673 if periods: 674 rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(periods[0].get_start_point()))) 675 rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(periods[-1].get_end_point()))) 676 else: 677 rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_start_point()))) 678 rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_end_point()))) 679 680 for p in periods: 681 if p.transp == "OPAQUE": 682 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join( 683 map(format_datetime, [p.get_start_point(), p.get_end_point()]) 684 ))) 685 686 return ("VFREEBUSY", {}, record) 687 688 def parse_calendar(f, encoding): 689 690 """ 691 Parse the iTIP content from 'f' having the given 'encoding'. Return a 692 mapping from object types to collections of calendar objects. 693 """ 694 695 cal = parse_object(f, encoding, "VCALENDAR") 696 d = {} 697 698 for objtype, values in cal.items(): 699 d[objtype] = l = [] 700 for value in values: 701 l.append(Object({objtype : value})) 702 703 return d 704 705 def parse_object(f, encoding, objtype=None): 706 707 """ 708 Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is 709 given, only objects of that type will be returned. Otherwise, the root of 710 the content will be returned as a dictionary with a single key indicating 711 the object type. 712 713 Return None if the content was not readable or suitable. 714 """ 715 716 try: 717 try: 718 doctype, attrs, elements = obj = parse(f, encoding=encoding) 719 if objtype and doctype == objtype: 720 return to_dict(obj)[objtype][0] 721 elif not objtype: 722 return to_dict(obj) 723 finally: 724 f.close() 725 726 # NOTE: Handle parse errors properly. 727 728 except (ParseError, ValueError): 729 pass 730 731 return None 732 733 def parse_string(s, encoding, objtype=None): 734 735 """ 736 Parse the iTIP content from 's' having the given 'encoding'. If 'objtype' is 737 given, only objects of that type will be returned. Otherwise, the root of 738 the content will be returned as a dictionary with a single key indicating 739 the object type. 740 741 Return None if the content was not readable or suitable. 742 """ 743 744 return parse_object(StringIO(s), encoding, objtype) 745 746 def to_part(method, fragments, encoding="utf-8", line_length=None): 747 748 """ 749 Write using the given 'method', the given 'fragments' to a MIME 750 text/calendar part. 751 """ 752 753 out = StringIO() 754 try: 755 to_stream(out, make_calendar(fragments, method), encoding, line_length) 756 part = MIMEText(out.getvalue(), "calendar", encoding) 757 part.set_param("method", method) 758 return part 759 760 finally: 761 out.close() 762 763 def to_stream(out, fragment, encoding="utf-8", line_length=None): 764 765 "Write to the 'out' stream the given 'fragment'." 766 767 iterwrite(out, encoding=encoding, line_length=line_length).append(fragment) 768 769 def to_string(fragment, encoding="utf-8", line_length=None): 770 771 "Return a string encoding the given 'fragment'." 772 773 out = StringIO() 774 try: 775 to_stream(out, fragment, encoding, line_length) 776 return out.getvalue() 777 778 finally: 779 out.close() 780 781 def new_object(object_type): 782 783 "Make a new object of the given 'object_type'." 784 785 return Object({object_type : ({}, {})}) 786 787 def make_uid(user): 788 789 "Return a unique identifier for a new object by the given 'user'." 790 791 utcnow = get_timestamp() 792 return "imip-agent-%s-%s" % (utcnow, get_address(user)) 793 794 # Structure access functions. 795 796 def get_items(d, name, all=True): 797 798 """ 799 Get all items from 'd' for the given 'name', returning single items if 800 'all' is specified and set to a false value and if only one value is 801 present for the name. Return None if no items are found for the name or if 802 many items are found but 'all' is set to a false value. 803 """ 804 805 if d.has_key(name): 806 items = [(value or None, attr) for value, attr in d[name]] 807 if all: 808 return items 809 elif len(items) == 1: 810 return items[0] 811 else: 812 return None 813 else: 814 return None 815 816 def get_item(d, name): 817 return get_items(d, name, False) 818 819 def get_value_map(d, name): 820 821 """ 822 Return a dictionary for all items in 'd' having the given 'name'. The 823 dictionary will map values for the name to any attributes or qualifiers 824 that may have been present. 825 """ 826 827 items = get_items(d, name) 828 if items: 829 return dict(items) 830 else: 831 return {} 832 833 def values_from_items(items): 834 return map(lambda x: x[0], items) 835 836 def get_values(d, name, all=True): 837 if d.has_key(name): 838 items = d[name] 839 if not all and len(items) == 1: 840 return items[0][0] 841 else: 842 return values_from_items(items) 843 else: 844 return None 845 846 def get_value(d, name): 847 return get_values(d, name, False) 848 849 def get_date_value_items(d, name, tzid=None): 850 851 """ 852 Obtain items from 'd' having the given 'name', where a single item yields 853 potentially many values. Return a list of tuples of the form (value, 854 attributes) where the attributes have been given for the property in 'd'. 855 """ 856 857 items = get_items(d, name) 858 if items: 859 all_items = [] 860 for item in items: 861 values, attr = item 862 if not attr.has_key("TZID") and tzid: 863 attr["TZID"] = tzid 864 if not isinstance(values, list): 865 values = [values] 866 for value in values: 867 all_items.append((get_datetime(value, attr) or get_period(value, attr), attr)) 868 return all_items 869 else: 870 return None 871 872 def get_date_value_item_periods(d, name, duration, tzid=None): 873 874 """ 875 Obtain items from 'd' having the given 'name', where a single item yields 876 potentially many values. The 'duration' must be provided to define the 877 length of periods having only a start datetime. Return a list of periods 878 corresponding to the property in 'd'. 879 """ 880 881 items = get_date_value_items(d, name, tzid) 882 if not items: 883 return items 884 885 periods = [] 886 887 for value, attr in items: 888 if isinstance(value, tuple): 889 periods.append(RecurringPeriod(value[0], value[1], tzid, name, attr)) 890 else: 891 periods.append(RecurringPeriod(value, value + duration, tzid, name, attr)) 892 893 return periods 894 895 def get_period_values(d, name, tzid=None): 896 897 """ 898 Return period values from 'd' for the given property 'name', using 'tzid' 899 where specified to indicate the time zone. 900 """ 901 902 values = [] 903 for value, attr in get_items(d, name) or []: 904 if not attr.has_key("TZID") and tzid: 905 attr["TZID"] = tzid 906 start, end = get_period(value, attr) 907 values.append(Period(start, end, tzid=tzid)) 908 return values 909 910 def get_utc_datetime(d, name, date_tzid=None): 911 912 """ 913 Return the value provided by 'd' for 'name' as a datetime in the UTC zone 914 or as a date, converting any date to a datetime if 'date_tzid' is specified. 915 If no datetime or date is available, None is returned. 916 """ 917 918 t = get_datetime_item(d, name) 919 if not t: 920 return None 921 else: 922 dt, attr = t 923 return dt is not None and to_utc_datetime(dt, date_tzid) or None 924 925 def get_datetime_item(d, name): 926 927 """ 928 Return the value provided by 'd' for 'name' as a datetime or as a date, 929 together with the attributes describing it. Return None if no value exists 930 for 'name' in 'd'. 931 """ 932 933 t = get_item(d, name) 934 if not t: 935 return None 936 else: 937 value, attr = t 938 dt = get_datetime(value, attr) 939 tzid = get_datetime_tzid(dt) 940 if tzid: 941 attr["TZID"] = tzid 942 return dt, attr 943 944 # Conversion functions. 945 946 def get_address_parts(values): 947 948 "Return name and address tuples for each of the given 'values'." 949 950 l = [] 951 for name, address in values and email.utils.getaddresses(values) or []: 952 if is_mailto_uri(name): 953 name = name[7:] # strip "mailto:" 954 l.append((name, address)) 955 return l 956 957 def get_addresses(values): 958 959 """ 960 Return only addresses from the given 'values' which may be of the form 961 "Common Name <recipient@domain>", with the latter part being the address 962 itself. 963 """ 964 965 return [address for name, address in get_address_parts(values)] 966 967 def get_address(value): 968 969 "Return an e-mail address from the given 'value'." 970 971 if not value: return None 972 return get_addresses([value])[0] 973 974 def get_verbose_address(value, attr=None): 975 976 """ 977 Return a verbose e-mail address featuring any name from the given 'value' 978 and any accompanying 'attr' dictionary. 979 """ 980 981 l = get_address_parts([value]) 982 if not l: 983 return value 984 name, address = l[0] 985 if not name: 986 name = attr and attr.get("CN") 987 if name and address: 988 return "%s <%s>" % (name, address) 989 else: 990 return address 991 992 def is_mailto_uri(value): 993 994 """ 995 Return whether 'value' is a mailto: URI, with the protocol potentially being 996 in upper case. 997 """ 998 999 return value.lower().startswith("mailto:") 1000 1001 def get_uri(value): 1002 1003 "Return a URI for the given 'value'." 1004 1005 if not value: return None 1006 1007 # Normalise to "mailto:" or return other URI form. 1008 1009 return is_mailto_uri(value) and ("mailto:%s" % value[7:]) or \ 1010 ":" in value and value or \ 1011 "mailto:%s" % get_address(value) 1012 1013 def uri_parts(values): 1014 1015 "Return any common name plus the URI for each of the given 'values'." 1016 1017 return [(name, get_uri(address)) for name, address in get_address_parts(values)] 1018 1019 uri_value = get_uri 1020 1021 def uri_values(values): 1022 return map(get_uri, values) 1023 1024 def uri_dict(d): 1025 return dict([(get_uri(key), value) for key, value in d.items()]) 1026 1027 def uri_item(item): 1028 return get_uri(item[0]), item[1] 1029 1030 def uri_items(items): 1031 return [(get_uri(value), attr) for value, attr in items] 1032 1033 # Operations on structure data. 1034 1035 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, ignore_dtstamp): 1036 1037 """ 1038 Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and 1039 'new_dtstamp', and the 'ignore_dtstamp' indication, whether the object 1040 providing the new information is really newer than the object providing the 1041 old information. 1042 """ 1043 1044 have_sequence = old_sequence is not None and new_sequence is not None 1045 is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence) 1046 1047 have_dtstamp = old_dtstamp and new_dtstamp 1048 is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp 1049 1050 is_old_sequence = have_sequence and ( 1051 int(new_sequence) < int(old_sequence) or 1052 is_same_sequence and is_old_dtstamp 1053 ) 1054 1055 return is_same_sequence and ignore_dtstamp or not is_old_sequence 1056 1057 def check_delegation(attendee_map, attendee, attendee_attr): 1058 1059 """ 1060 Using the 'attendee_map', check the attributes for the given 'attendee' 1061 provided as 'attendee_attr', following the delegation chain back to the 1062 delegators and forward again to yield the delegate identities in each 1063 case. Pictorially... 1064 1065 attendee -> DELEGATED-FROM -> delegator 1066 ? <- DELEGATED-TO <--- 1067 1068 Return whether 'attendee' was identified as a delegate by providing the 1069 identity of any delegators referencing the attendee. 1070 """ 1071 1072 delegators = [] 1073 1074 # The recipient should have a reference to the delegator. 1075 1076 delegated_from = attendee_attr and attendee_attr.get("DELEGATED-FROM") 1077 if delegated_from: 1078 1079 # Examine all delegators. 1080 1081 for delegator in delegated_from: 1082 delegator_attr = attendee_map.get(delegator) 1083 1084 # The delegator should have a reference to the recipient. 1085 1086 delegated_to = delegator_attr and delegator_attr.get("DELEGATED-TO") 1087 if delegated_to and attendee in delegated_to: 1088 delegators.append(delegator) 1089 1090 return delegators 1091 1092 def make_rule_period(start, duration, attr, tzid): 1093 1094 """ 1095 Make a period for the rule period starting at 'start' with the given 1096 'duration' employing the given datetime 'attr' and 'tzid'. 1097 """ 1098 1099 # Determine the resolution of the period. 1100 1101 create = len(start) == 3 and date or datetime 1102 start = to_timezone(create(*start), tzid) 1103 end = start + duration 1104 1105 # Create the period with accompanying metadata based on the main 1106 # period and event details. 1107 1108 return RecurringPeriod(start, end, tzid, "RRULE", attr) 1109 1110 def get_rule_periods(rrule, start, duration, attr, tzid, end, inclusive=False): 1111 1112 """ 1113 Return periods for the given 'rrule', employing the 'start', 'duration', 1114 datetime 'attr' and 'tzid'. 1115 1116 The specified 'end' datetime indicates the end of the window for which 1117 periods shall be computed. 1118 1119 If 'inclusive' is set to a true value, any period occurring at the 'end' 1120 will be included. 1121 """ 1122 1123 parameters = rrule and get_parameters(rrule) 1124 selector = get_rule(start, rrule) 1125 1126 until = parameters.get("UNTIL") 1127 1128 if until: 1129 until_dt = to_timezone(get_datetime(until, attr), tzid) 1130 end = end and min(until_dt, end) or until_dt 1131 inclusive = True 1132 1133 # Obtain period instances, starting from the main period. Since counting 1134 # must start from the first period, filtering from a start date must be 1135 # done after the instances have been obtained. 1136 1137 periods = [] 1138 1139 for recurrence_start in selector.materialise(start, end, 1140 parameters.get("COUNT"), 1141 parameters.get("BYSETPOS"), 1142 inclusive): 1143 1144 periods.append(make_rule_period(recurrence_start, duration, attr, tzid)) 1145 1146 return periods 1147 1148 def get_periods(obj, tzid, start=None, end=None, inclusive=False): 1149 1150 """ 1151 Return periods for the given object 'obj', employing the given 'tzid' where 1152 no time zone information is available (for whole day events, for example), 1153 confining materialised periods to after the given 'start' datetime and 1154 before the given 'end' datetime. 1155 1156 If 'end' is omitted, only explicit recurrences and recurrences from 1157 explicitly-terminated rules will be returned. 1158 1159 If 'inclusive' is set to a true value, any period occurring at the 'end' 1160 will be included. 1161 """ 1162 1163 rrule = obj.get_value("RRULE") 1164 parameters = rrule and get_parameters(rrule) 1165 1166 # Use localised datetimes. 1167 1168 main_period = obj.get_main_period(tzid) 1169 1170 dtstart = main_period.get_start() 1171 dtstart_attr = main_period.get_start_attr() 1172 1173 # Attempt to get time zone details from the object, using the supplied zone 1174 # only as a fallback. 1175 1176 obj_tzid = obj.get_tzid() 1177 1178 if not rrule: 1179 periods = [main_period] 1180 1181 # Recurrence rules create multiple instances to be checked. 1182 # Conflicts may only be assessed within a period defined by policy 1183 # for the agent, with instances outside that period being considered 1184 # unchecked. 1185 1186 elif end or parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT"): 1187 1188 # Define a selection period with a start point. The end will be handled 1189 # in the materialisation process. 1190 1191 selection_period = Period(start, None) 1192 periods = [] 1193 1194 for period in get_rule_periods(rrule, dtstart, 1195 main_period.get_duration(), dtstart_attr, obj_tzid or tzid, end, 1196 inclusive): 1197 1198 # Filter out periods before the start. 1199 1200 if period.within(selection_period): 1201 periods.append(period) 1202 1203 else: 1204 periods = [] 1205 1206 # Add recurrence dates. 1207 1208 rdates = obj.get_date_value_item_periods("RDATE", obj_tzid or tzid) 1209 if rdates: 1210 periods += rdates 1211 1212 # Return a sorted list of the periods. 1213 1214 periods.sort() 1215 1216 # Exclude exception dates. 1217 1218 exdates = obj.get_date_value_item_periods("EXDATE", obj_tzid or tzid) 1219 1220 if exdates: 1221 for period in exdates: 1222 i = bisect_left(periods, period) 1223 while i < len(periods) and periods[i] == period: 1224 del periods[i] 1225 1226 return periods 1227 1228 def get_sender_identities(mapping): 1229 1230 """ 1231 Return a mapping from actual senders to the identities for which they 1232 have provided data, extracting this information from the given 1233 'mapping'. The SENT-BY attribute provides sender information in preference 1234 to the property values given as the mapping keys. 1235 """ 1236 1237 senders = {} 1238 1239 for value, attr in mapping.items(): 1240 sent_by = attr.get("SENT-BY") 1241 if sent_by: 1242 sender = get_uri(sent_by) 1243 else: 1244 sender = value 1245 1246 if not senders.has_key(sender): 1247 senders[sender] = [] 1248 1249 senders[sender].append(value) 1250 1251 return senders 1252 1253 def get_window_end(tzid, days=100, start=None): 1254 1255 """ 1256 Return a datetime in the time zone indicated by 'tzid' marking the end of a 1257 window of the given number of 'days'. If 'start' is not indicated, the start 1258 of the window will be the current moment. 1259 """ 1260 1261 return to_timezone(start or datetime.now(), tzid) + timedelta(days) 1262 1263 def update_attendees_with_delegates(stored_attendees, attendees): 1264 1265 """ 1266 Update the 'stored_attendees' mapping with delegate information from the 1267 given 'attendees' mapping. 1268 """ 1269 1270 # Check for delegated attendees. 1271 1272 for attendee, attendee_attr in attendees.items(): 1273 1274 # Identify delegates and check the delegation using the updated 1275 # attendee information. 1276 1277 if not stored_attendees.has_key(attendee) and \ 1278 attendee_attr.has_key("DELEGATED-FROM") and \ 1279 check_delegation(stored_attendees, attendee, attendee_attr): 1280 1281 stored_attendees[attendee] = attendee_attr 1282 1283 # vim: tabstop=4 expandtab shiftwidth=4