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