imip-agent

imiptools/data.py

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