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