imip-agent

imiptools/client.py

1194:9c254032288d
2016-06-05 Paul Boddie Updated references to now-removed tables and directories.
     1 #!/usr/bin/env python     2      3 """     4 Common calendar client utilities.     5      6 Copyright (C) 2014, 2015, 2016 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 datetime import datetime, timedelta    23 from imiptools import config    24 from imiptools.data import Object, check_delegation, get_address, get_uri, \    25                            get_window_end, is_new_object, make_freebusy, to_part, \    26                            uri_dict, uri_item, uri_items, uri_parts, uri_values    27 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \    28                             get_duration, get_timestamp    29 from imiptools.i18n import get_translator    30 from imiptools.period import SupportAttendee, SupportExpires    31 from imiptools.profile import Preferences    32 from imiptools.stores import get_store, get_publisher, get_journal    33     34 class Client:    35     36     "Common handler and manager methods."    37     38     default_window_size = 100    39     organiser_methods = "ADD", "CANCEL", "DECLINECOUNTER", "PUBLISH", "REQUEST"    40     41     def __init__(self, user, messenger=None, store=None, publisher=None, journal=None,    42                  preferences_dir=None):    43     44         """    45         Initialise a calendar client with the current 'user', plus any    46         'messenger', 'store', 'publisher' and 'journal' objects, indicating any    47         specific 'preferences_dir'.    48         """    49     50         self.user = user    51         self.messenger = messenger    52         self.store = store or get_store(config.STORE_TYPE, config.STORE_DIR)    53         self.journal = journal or get_journal(config.STORE_TYPE, config.JOURNAL_DIR)    54     55         try:    56             self.publisher = publisher or get_publisher(config.PUBLISH_DIR)    57         except OSError:    58             self.publisher = None    59     60         self.preferences_dir = preferences_dir    61         self.preferences = None    62     63         # Localise the messenger.    64     65         if self.messenger:    66             self.messenger.gettext = self.get_translator()    67     68     def get_store(self):    69         return self.store    70     71     def get_publisher(self):    72         return self.publisher    73     74     def get_journal(self):    75         return self.journal    76     77     # Store-related methods.    78     79     def acquire_lock(self):    80         self.store.acquire_lock(self.user)    81     82     def release_lock(self):    83         self.store.release_lock(self.user)    84     85     # Preferences-related methods.    86     87     def get_preferences(self):    88         if not self.preferences and self.user:    89             self.preferences = Preferences(self.user, self.preferences_dir)    90         return self.preferences    91     92     def get_locale(self):    93         prefs = self.get_preferences()    94         return prefs and prefs.get("LANG", "en", True) or "en"    95     96     def get_translator(self):    97         return get_translator([self.get_locale()])    98     99     def get_user_attributes(self):   100         prefs = self.get_preferences()   101         return prefs and prefs.get_all(["CN"]) or {}   102    103     def get_tzid(self):   104         prefs = self.get_preferences()   105         return prefs and prefs.get("TZID") or get_default_timezone()   106    107     def get_window_size(self):   108         prefs = self.get_preferences()   109         try:   110             return prefs and int(prefs.get("window_size")) or self.default_window_size   111         except (TypeError, ValueError):   112             return self.default_window_size   113    114     def get_window_end(self):   115         return get_window_end(self.get_tzid(), self.get_window_size())   116    117     def is_participating(self):   118    119         "Return participation in the calendar system."   120    121         prefs = self.get_preferences()   122         return prefs and prefs.get("participating", config.PARTICIPATING_DEFAULT) != "no" or False   123    124     def is_sharing(self):   125    126         "Return whether free/busy information is being generally shared."   127    128         prefs = self.get_preferences()   129         return prefs and prefs.get("freebusy_sharing", config.SHARING_DEFAULT) == "share" or False   130    131     def is_bundling(self):   132    133         "Return whether free/busy information is being bundled in messages."   134    135         prefs = self.get_preferences()   136         return prefs and prefs.get("freebusy_bundling", config.BUNDLING_DEFAULT) == "always" or False   137    138     def is_notifying(self):   139    140         "Return whether recipients are notified about free/busy payloads."   141    142         prefs = self.get_preferences()   143         return prefs and prefs.get("freebusy_messages", config.NOTIFYING_DEFAULT) == "notify" or False   144    145     def is_publishing(self):   146    147         "Return whether free/busy information is being published as Web resources."   148    149         prefs = self.get_preferences()   150         return prefs and prefs.get("freebusy_publishing", config.PUBLISHING_DEFAULT) == "publish" or False   151    152     def is_refreshing(self):   153    154         "Return whether a recipient supports requests to refresh event details."   155    156         prefs = self.get_preferences()   157         return prefs and prefs.get("event_refreshing", config.REFRESHING_DEFAULT) == "always" or False   158    159     def allow_add(self):   160         return self.get_add_method_response() in ("add", "refresh")   161    162     def get_add_method_response(self):   163         prefs = self.get_preferences()   164         return prefs and prefs.get("add_method_response", config.ADD_RESPONSE_DEFAULT) or "refresh"   165    166     def get_offer_period(self):   167    168         "Decode a specification in the iCalendar duration format."   169    170         prefs = self.get_preferences()   171         duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT)   172    173         # NOTE: Should probably report an error somehow if None.   174    175         return duration and get_duration(duration) or None   176    177     def get_organiser_replacement(self):   178         prefs = self.get_preferences()   179         return prefs and prefs.get("organiser_replacement", config.ORGANISER_REPLACEMENT_DEFAULT) or "attendee"   180    181     def have_manager(self):   182         return config.MANAGER_INTERFACE   183    184     def get_permitted_values(self):   185    186         """   187         Decode a specification of one of the following forms...   188    189         <minute values>   190         <hour values>:<minute values>   191         <hour values>:<minute values>:<second values>   192    193         ...with each list of values being comma-separated.   194         """   195    196         prefs = self.get_preferences()   197         permitted_values = prefs and prefs.get("permitted_times")   198         if permitted_values:   199             try:   200                 l = []   201                 for component in permitted_values.split(":")[:3]:   202                     if component:   203                         l.append(map(int, component.split(",")))   204                     else:   205                         l.append(None)   206    207             # NOTE: Should probably report an error somehow.   208    209             except ValueError:   210                 return None   211             else:   212                 l = (len(l) < 2 and [None] or []) + l + (len(l) < 3 and [None] or [])   213                 return l   214         else:   215             return None   216    217     # Common operations on calendar data.   218    219     def update_sender(self, attr):   220    221         "Update the SENT-BY attribute of the 'attr' sender metadata."   222    223         if self.messenger and self.messenger.sender != get_address(self.user):   224             attr["SENT-BY"] = get_uri(self.messenger.sender)   225    226     def get_periods(self, obj, explicit_only=False):   227    228         """   229         Return periods for the given 'obj'. Interpretation of periods can depend   230         on the time zone, which is obtained for the current user. If   231         'explicit_only' is set to a true value, only explicit periods will be   232         returned, not rule-based periods.   233         """   234    235         return obj.get_periods(self.get_tzid(), not explicit_only and self.get_window_end() or None)   236    237     # Store operations.   238    239     def get_stored_object(self, uid, recurrenceid, section=None, username=None):   240    241         """   242         Return the stored object for the current user, with the given 'uid' and   243         'recurrenceid' from the given 'section' and for the given 'username' (if   244         specified), or from the standard object collection otherwise.   245         """   246    247         if section == "counters":   248             fragment = self.store.get_counter(self.user, username, uid, recurrenceid)   249         else:   250             fragment = self.store.get_event(self.user, uid, recurrenceid, section)   251         return fragment and Object(fragment)   252    253     # Free/busy operations.   254    255     def get_freebusy_part(self, freebusy=None):   256    257         """   258         Return a message part containing free/busy information for the user,   259         either specified as 'freebusy' or obtained from the store directly.   260         """   261    262         if self.is_sharing() and self.is_bundling():   263    264             # Invent a unique identifier.   265    266             utcnow = get_timestamp()   267             uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))   268    269             freebusy = freebusy or self.store.get_freebusy(self.user)   270    271             user_attr = {}   272             self.update_sender(user_attr)   273             return self.to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])   274    275         return None   276    277     def update_freebusy(self, freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):   278    279         """   280         Update the 'freebusy' collection with the given 'periods', indicating a   281         'transp' status, explicit 'uid' and 'recurrenceid' to indicate either a   282         recurrence or the parent event. The 'summary' and 'organiser' must also   283         be provided.   284    285         An optional 'expires' datetime string can be provided to tag a free/busy   286         offer.   287         """   288    289         # Add specific attendee information for certain collections.   290    291         if isinstance(freebusy, SupportAttendee):   292             freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, self.user)   293    294         # Add expiry datetime for certain collections.   295    296         elif isinstance(freebusy, SupportExpires):   297             freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires)   298    299         # Provide only the essential attributes for other collections.   300    301         else:   302             freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser)   303    304     # Preparation of content.   305    306     def to_part(self, method, fragments):   307    308         "Return an encoded MIME part for the given 'method' and 'fragments'."   309    310         return to_part(method, fragments, line_length=config.CALENDAR_LINE_LENGTH)   311    312     def object_to_part(self, method, obj):   313    314         "Return an encoded MIME part for the given 'method' and 'obj'."   315    316         return obj.to_part(method, line_length=config.CALENDAR_LINE_LENGTH)   317    318     # Preparation of messages communicating the state of events.   319    320     def get_message_parts(self, obj, method, attendee=None):   321    322         """   323         Return a tuple containing a list of methods and a list of message parts,   324         with the parts collectively describing the given object 'obj' and its   325         recurrences, using 'method' as the means of publishing details (with   326         CANCEL being used to retract or remove details).   327    328         If 'attendee' is indicated, the attendee's participation will be taken   329         into account when generating the description.   330         """   331    332         # Assume that the outcome will be composed of requests and   333         # cancellations. It would not seem completely bizarre to produce   334         # publishing messages if a refresh message was unprovoked.   335    336         responses = []   337         methods = set()   338    339         # Get the parent event, add SENT-BY details to the organiser.   340    341         if not attendee or self.is_participating(attendee, obj=obj):   342             organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))   343             self.update_sender(organiser_attr)   344             responses.append(self.object_to_part(method, obj))   345             methods.add(method)   346    347         # Get recurrences for parent events.   348    349         if not self.recurrenceid:   350    351             # Collect active and cancelled recurrences.   352    353             for rl, section, rmethod in [   354                 (self.store.get_active_recurrences(self.user, self.uid), None, method),   355                 (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"),   356                 ]:   357    358                 for recurrenceid in rl:   359    360                     # Get the recurrence, add SENT-BY details to the organiser.   361    362                     obj = self.get_stored_object(self.uid, recurrenceid, section)   363    364                     if not attendee or self.is_participating(attendee, obj=obj):   365                         organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))   366                         self.update_sender(organiser_attr)   367                         responses.append(self.object_to_part(rmethod, obj))   368                         methods.add(rmethod)   369    370         return methods, responses   371    372 class ClientForObject(Client):   373    374     "A client maintaining a specific object."   375    376     def __init__(self, obj, user, messenger=None, store=None, publisher=None,   377                  journal=None, preferences_dir=None):   378         Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir)   379         self.set_object(obj)   380    381     def set_object(self, obj):   382    383         "Set the current object to 'obj', obtaining metadata details."   384    385         self.obj = obj   386         self.uid = obj and self.obj.get_uid()   387         self.recurrenceid = obj and self.obj.get_recurrenceid()   388         self.sequence = obj and self.obj.get_value("SEQUENCE")   389         self.dtstamp = obj and self.obj.get_value("DTSTAMP")   390    391     def set_identity(self, method):   392    393         """   394         Set the current user for the current object in the context of the given   395         'method'. It is usually set when initialising the handler, using the   396         recipient details, but outgoing messages do not reference the recipient   397         in this way.   398         """   399    400         pass   401    402     def is_usable(self, method=None):   403    404         "Return whether the current object is usable with the given 'method'."   405    406         return True   407    408     def is_organiser(self):   409    410         """   411         Return whether the current user is the organiser in the current object.   412         """   413    414         return get_uri(self.obj.get_value("ORGANIZER")) == self.user   415    416     def is_recurrence(self):   417    418         "Return whether the current object is a recurrence of its parent."   419    420         parent = self.get_parent_object()   421         return parent and parent.has_recurrence(self.get_tzid(), self.obj.get_recurrenceid())   422    423     # Common operations on calendar data.   424    425     def update_senders(self, obj=None):   426    427         """   428         Update sender details in 'obj', or the current object if not indicated,   429         removing SENT-BY attributes for attendees other than the current user if   430         those attributes give the URI of the calendar system.   431         """   432    433         obj = obj or self.obj   434         calendar_uri = self.messenger and get_uri(self.messenger.sender)   435         for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")):   436             if attendee != self.user:   437                 if attendee_attr.get("SENT-BY") == calendar_uri:   438                     del attendee_attr["SENT-BY"]   439             else:   440                 attendee_attr["SENT-BY"] = calendar_uri   441    442     def get_sending_attendee(self):   443    444         "Return the attendee who sent the current object."   445    446         # Search for the sender of the message or the calendar system address.   447    448         senders = self.senders or self.messenger and [self.messenger.sender] or []   449    450         for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):   451             if get_address(attendee) in senders or \   452                get_address(attendee_attr.get("SENT-BY")) in senders:   453                 return get_uri(attendee)   454    455         return None   456    457     def get_unscheduled_parts(self, periods):   458    459         "Return message parts describing unscheduled 'periods'."   460    461         unscheduled_parts = []   462    463         if periods:   464             obj = self.obj.copy()   465             obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])   466    467             for p in periods:   468                 if not p.origin:   469                     continue   470                 obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())]   471                 obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())]   472                 unscheduled_parts.append(self.object_to_part("CANCEL", obj))   473    474         return unscheduled_parts   475    476     # Object update methods.   477    478     def update_recurrenceid(self):   479    480         """   481         Update the RECURRENCE-ID in the current object, initialising it from   482         DTSTART.   483         """   484    485         self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")]   486         self.recurrenceid = self.obj.get_recurrenceid()   487    488     def update_dtstamp(self, obj=None):   489    490         "Update the DTSTAMP in the current object or any given object 'obj'."   491    492         obj = obj or self.obj   493         self.dtstamp = obj.update_dtstamp()   494    495     def update_sequence(self, increment=False, obj=None):   496    497         "Update the SEQUENCE in the current object or any given object 'obj'."   498    499         obj = obj or self.obj   500         obj.update_sequence(increment)   501    502     def merge_attendance(self, attendees):   503    504         """   505         Merge attendance from the current object's 'attendees' into the version   506         stored for the current user.   507         """   508    509         obj = self.get_stored_object_version()   510    511         if not obj or not self.have_new_object():   512             return False   513    514         # Get attendee details in a usable form.   515    516         attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))   517    518         for attendee, attendee_attr in attendees.items():   519    520             # Update attendance in the loaded object for any recognised   521             # attendees.   522    523             if attendee_map.has_key(attendee):   524                 attendee_map[attendee] = attendee_attr   525    526         # Check for delegated attendees.   527    528         for attendee, attendee_attr in attendees.items():   529    530             # Identify delegates and check the delegation using the updated   531             # attendee information.   532    533             if not attendee_map.has_key(attendee) and \   534                attendee_attr.has_key("DELEGATED-FROM") and \   535                check_delegation(attendee_map, attendee, attendee_attr):   536    537                 attendee_map[attendee] = attendee_attr   538    539         # Set the new details and store the object.   540    541         obj["ATTENDEE"] = attendee_map.items()   542    543         # Set a specific recurrence or the complete event if not an additional   544         # occurrence.   545    546         return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node())   547    548     def update_attendees(self, attendees, removed):   549    550         """   551         Update the attendees in the current object with the given 'attendees'   552         and 'removed' attendee lists.   553    554         A tuple is returned containing two items: a list of the attendees whose   555         attendance is being proposed (in a counter-proposal), a list of the   556         attendees whose attendance should be cancelled.   557         """   558    559         to_cancel = []   560    561         existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or [])   562         existing_attendees_map = dict(existing_attendees)   563    564         # Added attendees are those from the supplied collection not already   565         # present in the object.   566    567         added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees])   568         removed = uri_values(removed)   569    570         if added or removed:   571    572             # The organiser can remove existing attendees.   573    574             if removed and self.is_organiser():   575                 remaining = []   576    577                 for attendee, attendee_attr in existing_attendees:   578                     if attendee in removed:   579    580                         # Only when an event has not been published can   581                         # attendees be silently removed.   582    583                         if self.obj.is_shared():   584                             to_cancel.append((attendee, attendee_attr))   585                     else:   586                         remaining.append((attendee, attendee_attr))   587    588                 existing_attendees = remaining   589    590             # Attendees (when countering) must only include the current user and   591             # any added attendees.   592    593             elif not self.is_organiser():   594                 existing_attendees = []   595    596             # Both organisers and attendees (when countering) can add attendees.   597    598             if added:   599    600                 # Obtain a mapping from URIs to name details.   601    602                 attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)])   603    604                 for attendee in added:   605                     attendee = attendee.strip()   606                     if attendee:   607                         cn = attendee_map.get(attendee)   608                         attendee_attr = {"CN" : cn} or {}   609    610                         # Only the organiser can reset the participation attributes.   611    612                         if self.is_organiser():   613                             attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})   614    615                         existing_attendees.append((attendee, attendee_attr))   616    617             # Attendees (when countering) must only include the current user and   618             # any added attendees.   619    620             if not self.is_organiser() and self.user not in existing_attendees:   621                 user_attr = self.get_user_attributes()   622                 user_attr.update(existing_attendees_map.get(self.user) or {})   623                 existing_attendees.append((self.user, user_attr))   624    625             self.obj["ATTENDEE"] = existing_attendees   626    627         return added, to_cancel   628    629     def update_participation(self, partstat=None):   630    631         """   632         Update the participation in the current object of the user with the   633         given 'partstat'.   634         """   635    636         attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE")).get(self.user)   637         if not attendee_attr:   638             return None   639         if partstat:   640             attendee_attr["PARTSTAT"] = partstat   641         if attendee_attr.has_key("RSVP"):   642             del attendee_attr["RSVP"]   643         self.update_sender(attendee_attr)   644         return attendee_attr   645    646     # Communication methods.   647    648     def send_message(self, parts, sender, obj, from_organiser, bcc_sender):   649    650         """   651         Send the given 'parts' to the appropriate recipients, also sending a   652         copy to the 'sender'. The 'obj' together with the 'from_organiser' value   653         (which indicates whether the organiser is sending this message) are used   654         to determine the recipients of the message.   655         """   656    657         # As organiser, send an invitation to attendees, excluding oneself if   658         # also attending. The updated event will be saved by the outgoing   659         # handler.   660    661         organiser = get_uri(obj.get_value("ORGANIZER"))   662         attendees = uri_values(obj.get_values("ATTENDEE"))   663    664         if from_organiser:   665             recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]   666         else:   667             recipients = [get_address(organiser)]   668    669         # Since the outgoing handler updates this user's free/busy details,   670         # the stored details will probably not have the updated details at   671         # this point, so we update our copy for serialisation as the bundled   672         # free/busy object.   673    674         freebusy = self.store.get_freebusy(self.user).copy()   675         self.update_freebusy(freebusy, self.user, from_organiser)   676    677         # Bundle free/busy information if appropriate.   678    679         part = self.get_freebusy_part(freebusy)   680         if part:   681             parts.append(part)   682    683         if recipients or bcc_sender:   684             self._send_message(sender, recipients, parts, bcc_sender)   685    686     def _send_message(self, sender, recipients, parts, bcc_sender):   687    688         """   689         Send a message, explicitly specifying the 'sender' as an outgoing BCC   690         recipient since the generic calendar user will be the actual sender.   691         """   692    693         if not self.messenger:   694             return   695    696         if not bcc_sender:   697             message = self.messenger.make_outgoing_message(parts, recipients)   698             self.messenger.sendmail(recipients, message.as_string())   699         else:   700             message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)   701             self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)   702    703     def send_message_to_self(self, parts):   704    705         "Send a message composed of the given 'parts' to the given user."   706    707         if not self.messenger:   708             return   709    710         sender = get_address(self.user)   711         message = self.messenger.make_outgoing_message(parts, [sender])   712         self.messenger.sendmail([sender], message.as_string())   713    714     # Action methods.   715    716     def process_declined_counter(self, attendee):   717    718         "Process a declined counter-proposal."   719    720         # Obtain the counter-proposal for the attendee.   721    722         obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)   723         if not obj:   724             return False   725    726         method = "DECLINECOUNTER"   727         self.update_senders(obj=obj)   728         obj.update_dtstamp()   729         obj.update_sequence(False)   730         self._send_message(get_address(self.user), [get_address(attendee)], [self.object_to_part(method, obj)], True)   731         return True   732    733     def process_received_request(self, changed=False):   734    735         """   736         Process the current request for the current user. Return whether any   737         action was taken. If 'changed' is set to a true value, or if 'attendees'   738         is specified and differs from the stored attendees, a counter-proposal   739         will be sent instead of a reply.   740         """   741    742         # Reply only on behalf of this user.   743    744         attendee_attr = self.update_participation()   745    746         if not attendee_attr:   747             return False   748    749         if not changed:   750             self.obj["ATTENDEE"] = [(self.user, attendee_attr)]   751         else:   752             self.update_senders()   753    754         self.update_dtstamp()   755         self.update_sequence(False)   756         self.send_message([self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)],   757                           get_address(self.user), self.obj, False, True)   758         return True   759    760     def process_created_request(self, method, to_cancel=None, to_unschedule=None):   761    762         """   763         Process the current request, sending a created request of the given   764         'method' to attendees. Return whether any action was taken.   765    766         If 'to_cancel' is specified, a list of participants to be sent cancel   767         messages is provided.   768    769         If 'to_unschedule' is specified, a list of periods to be unscheduled is   770         provided.   771         """   772    773         # Here, the organiser should be the current user.   774    775         organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))   776    777         self.update_sender(organiser_attr)   778         self.update_senders()   779         self.update_dtstamp()   780         self.update_sequence(True)   781    782         if method == "REQUEST":   783             methods, parts = self.get_message_parts(self.obj, "REQUEST")   784    785             # Add message parts with cancelled occurrence information.   786    787             unscheduled_parts = self.get_unscheduled_parts(to_unschedule)   788    789             # Send the updated event, along with a cancellation for each of the   790             # unscheduled occurrences.   791    792             self.send_message(parts + unscheduled_parts, get_address(organiser), self.obj, True, False)   793    794             # Since the organiser can update the SEQUENCE but this can leave any   795             # mail/calendar client lagging, issue a PUBLISH message to the   796             # user's address.   797    798             methods, parts = self.get_message_parts(self.obj, "PUBLISH")   799             self.send_message_to_self(parts + unscheduled_parts)   800    801         # When cancelling, replace the attendees with those for whom the event   802         # is now cancelled.   803    804         if method == "CANCEL" or to_cancel:   805             if to_cancel:   806                 obj = self.obj.copy()   807                 obj["ATTENDEE"] = to_cancel   808             else:   809                 obj = self.obj   810    811             # Send a cancellation to all uninvited attendees.   812    813             parts = [self.object_to_part("CANCEL", obj)]   814             self.send_message(parts, get_address(organiser), obj, True, False)   815    816             # Issue a CANCEL message to the user's address.   817    818             if method == "CANCEL":   819                 self.send_message_to_self(parts)   820    821         return True   822    823     # Object-related tests.   824    825     def is_recognised_organiser(self, organiser):   826    827         """   828         Return whether the given 'organiser' is recognised from   829         previously-received details. If no stored details exist, True is   830         returned.   831         """   832    833         obj = self.get_stored_object_version()   834         if obj:   835             stored_organiser = get_uri(obj.get_value("ORGANIZER"))   836             return stored_organiser == organiser   837         else:   838             return True   839    840     def is_recognised_attendee(self, attendee):   841    842         """   843         Return whether the given 'attendee' is recognised from   844         previously-received details. If no stored details exist, True is   845         returned.   846         """   847    848         obj = self.get_stored_object_version()   849         if obj:   850             stored_attendees = uri_dict(obj.get_value_map("ATTENDEE"))   851             return stored_attendees.has_key(attendee)   852         else:   853             return True   854    855     def get_attendance(self, user=None, obj=None):   856    857         """   858         Return the attendance attributes for 'user', or the current user if   859         'user' is not specified.   860         """   861    862         attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE"))   863         return attendees.get(user or self.user)   864    865     def is_participating(self, user, as_organiser=False, obj=None):   866    867         """   868         Return whether, subject to the 'user' indicating an identity and the   869         'as_organiser' status of that identity, the user concerned is actually   870         participating in the current object event.   871         """   872    873         # Use any attendee property information for an organiser, not the   874         # organiser property attributes.   875    876         attr = self.get_attendance(user, obj)   877         return as_organiser or attr is not None and not attr or \   878             attr and attr.get("PARTSTAT") not in ("DECLINED", "DELEGATED", "NEEDS-ACTION")   879    880     def has_indicated_attendance(self, user=None, obj=None):   881    882         """   883         Return whether the given 'user' (or the current user if not specified)   884         has indicated attendance in the given 'obj' (or the current object if   885         not specified).   886         """   887    888         attr = self.get_attendance(user, obj)   889         return attr and attr.get("PARTSTAT") not in (None, "NEEDS-ACTION")   890    891     def get_overriding_transparency(self, user, as_organiser=False):   892    893         """   894         Return the overriding transparency to be associated with the free/busy   895         records for an event, subject to the 'user' indicating an identity and   896         the 'as_organiser' status of that identity.   897    898         Where an identity is only an organiser and not attending, "ORG" is   899         returned. Otherwise, no overriding transparency is defined and None is   900         returned.   901         """   902    903         attr = self.get_attendance(user)   904         return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None   905    906     def can_schedule(self, freebusy, periods):   907    908         """   909         Indicate whether within 'freebusy' the given 'periods' can be scheduled.   910         """   911    912         return freebusy.can_schedule(periods, self.uid, self.recurrenceid)   913    914     def have_new_object(self, strict=True):   915    916         """   917         Return whether the current object is new to the current user.   918    919         If 'strict' is specified and is a false value, the DTSTAMP test will be   920         ignored. This is useful in handling responses from attendees from   921         clients (like Claws Mail) that erase time information from DTSTAMP and   922         make it invalid.   923         """   924    925         obj = self.get_stored_object_version()   926    927         # If found, compare SEQUENCE and potentially DTSTAMP.   928    929         if obj:   930             sequence = obj.get_value("SEQUENCE")   931             dtstamp = obj.get_value("DTSTAMP")   932    933             # If the request refers to an older version of the object, ignore   934             # it.   935    936             return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp, not strict)   937    938         return True   939    940     def possibly_recurring_indefinitely(self):   941    942         "Return whether the object recurs indefinitely."   943    944         # Obtain the stored object to make sure that recurrence information   945         # is not being ignored. This might happen if a client sends a   946         # cancellation without the complete set of properties, for instance.   947    948         return self.obj.possibly_recurring_indefinitely() or \   949                self.get_stored_object_version() and \   950                self.get_stored_object_version().possibly_recurring_indefinitely()   951    952     # Constraint application on event periods.   953    954     def check_object(self):   955    956         "Check the object against any scheduling constraints."   957    958         permitted_values = self.get_permitted_values()   959         if not permitted_values:   960             return None   961    962         invalid = []   963    964         for period in self.obj.get_periods(self.get_tzid()):   965             errors = period.check_permitted(permitted_values)   966             if errors:   967                 start_errors, end_errors = errors   968                 invalid.append((period.origin, start_errors, end_errors))   969    970         return invalid   971    972     def correct_object(self):   973    974         "Correct the object according to any scheduling constraints."   975    976         permitted_values = self.get_permitted_values()   977         return permitted_values and self.obj.correct_object(self.get_tzid(), permitted_values)   978    979     def correct_period(self, period):   980    981         "Correct 'period' according to any scheduling constraints."   982    983         permitted_values = self.get_permitted_values()   984         if not permitted_values:   985             return period   986         else:   987             return period.get_corrected(permitted_values)   988    989     # Object retrieval.   990    991     def get_stored_object_version(self):   992    993         """   994         Return the stored object to which the current object refers for the   995         current user.   996         """   997    998         return self.get_stored_object(self.uid, self.recurrenceid)   999   1000     def get_definitive_object(self, as_organiser):  1001   1002         """  1003         Return an object considered definitive for the current transaction,  1004         using 'as_organiser' to select the current transaction's object if  1005         false, or selecting a stored object if true.  1006         """  1007   1008         return not as_organiser and self.obj or self.get_stored_object_version()  1009   1010     def get_parent_object(self):  1011   1012         """  1013         Return the parent object to which the current object refers for the  1014         current user.  1015         """  1016   1017         return self.recurrenceid and self.get_stored_object(self.uid, None) or None  1018   1019     def revert_cancellations(self, periods):  1020   1021         """  1022         Restore cancelled recurrences corresponding to any of the given  1023         'periods'.  1024         """  1025   1026         for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid):  1027             obj = self.get_stored_object(self.uid, recurrenceid, "cancellations")  1028             if set(self.get_periods(obj)).intersection(periods):  1029                 self.store.remove_cancellation(self.user, self.uid, recurrenceid)  1030   1031     # Convenience methods for modifying free/busy collections.  1032   1033     def get_recurrence_start_point(self, recurrenceid):  1034   1035         "Get 'recurrenceid' in a form suitable for matching free/busy entries."  1036   1037         return self.obj.get_recurrence_start_point(recurrenceid, self.get_tzid())  1038   1039     def remove_from_freebusy(self, freebusy):  1040   1041         "Remove this event from the given 'freebusy' collection."  1042   1043         removed = freebusy.remove_event_periods(self.uid, self.recurrenceid)  1044         if not removed and self.recurrenceid:  1045             return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid))  1046         else:  1047             return removed  1048   1049     def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):  1050   1051         """  1052         Remove from 'freebusy' any original recurrence from parent free/busy  1053         details for the current object, if the current object is a specific  1054         additional recurrence. Otherwise, remove all additional recurrence  1055         information corresponding to 'recurrenceids', or if omitted, all  1056         recurrences.  1057         """  1058   1059         if self.recurrenceid:  1060             recurrenceid = self.get_recurrence_start_point(self.recurrenceid)  1061             freebusy.remove_affected_period(self.uid, recurrenceid)  1062         else:  1063             # Remove obsolete recurrence periods.  1064   1065             freebusy.remove_additional_periods(self.uid, recurrenceids)  1066   1067             # Remove original periods affected by additional recurrences.  1068   1069             if recurrenceids:  1070                 for recurrenceid in recurrenceids:  1071                     recurrenceid = self.get_recurrence_start_point(recurrenceid)  1072                     freebusy.remove_affected_period(self.uid, recurrenceid)  1073   1074     def update_freebusy(self, freebusy, user, as_organiser, offer=False):  1075   1076         """  1077         Update the 'freebusy' collection for this event with the periods and  1078         transparency associated with the current object, subject to the 'user'  1079         identity and the attendance details provided for them, indicating  1080         whether the update is being done 'as_organiser' (for the organiser of  1081         an event) or not.  1082   1083         If 'offer' is set to a true value, any free/busy updates will be tagged  1084         with an expiry time.  1085         """  1086   1087         # Obtain the stored object if the current object is not issued by the  1088         # organiser. Attendees do not have the opportunity to redefine the  1089         # periods.  1090   1091         obj = self.get_definitive_object(as_organiser)  1092         if not obj:  1093             return  1094   1095         # Obtain the affected periods.  1096   1097         periods = self.get_periods(obj)  1098   1099         # Define an overriding transparency, the indicated event transparency,  1100         # or the default transparency for the free/busy entry.  1101   1102         transp = self.get_overriding_transparency(user, as_organiser) or \  1103                  obj.get_value("TRANSP") or \  1104                  "OPAQUE"  1105   1106         # Calculate any expiry time. If no offer period is defined, do not  1107         # record the offer periods.  1108   1109         if offer:  1110             offer_period = self.get_offer_period()  1111             if offer_period:  1112                 expires = get_timestamp(offer_period)  1113             else:  1114                 return  1115         else:  1116             expires = None  1117   1118         # Perform the low-level update.  1119   1120         Client.update_freebusy(self, freebusy, periods, transp,  1121             self.uid, self.recurrenceid,  1122             obj.get_value("SUMMARY"),  1123             get_uri(obj.get_value("ORGANIZER")),  1124             expires)  1125   1126     def update_freebusy_for_participant(self, freebusy, user, for_organiser=False,  1127                                         updating_other=False, offer=False):  1128   1129         """  1130         Update the 'freebusy' collection for the given 'user', indicating  1131         whether the update is 'for_organiser' (being done for the organiser of  1132         an event) or not, and whether it is 'updating_other' (meaning another  1133         user's details).  1134   1135         If 'offer' is set to a true value, any free/busy updates will be tagged  1136         with an expiry time.  1137         """  1138   1139         # Record in the free/busy details unless a non-participating attendee.  1140         # Remove periods for non-participating attendees.  1141   1142         if offer or self.is_participating(user, for_organiser and not updating_other):  1143             self.update_freebusy(freebusy, user,  1144                 for_organiser and not updating_other or  1145                 not for_organiser and updating_other,  1146                 offer  1147                 )  1148         else:  1149             self.remove_from_freebusy(freebusy)  1150   1151     def remove_freebusy_for_participant(self, freebusy, user, for_organiser=False,  1152                                         updating_other=False):  1153   1154         """  1155         Remove details from the 'freebusy' collection for the given 'user',  1156         indicating whether the modification is 'for_organiser' (being done for  1157         the organiser of an event) or not, and whether it is 'updating_other'  1158         (meaning another user's details).  1159         """  1160   1161         # Remove from the free/busy details if a specified attendee.  1162   1163         if self.is_participating(user, for_organiser and not updating_other):  1164             self.remove_from_freebusy(freebusy)  1165   1166     # Convenience methods for updating stored free/busy information received  1167     # from other users.  1168   1169     def update_freebusy_from_participant(self, user, for_organiser, fn=None):  1170   1171         """  1172         For the current user, record the free/busy information for another  1173         'user', indicating whether the update is 'for_organiser' or not, thus  1174         maintaining a separate record of their free/busy details.  1175         """  1176   1177         fn = fn or self.update_freebusy_for_participant  1178   1179         # A user does not store free/busy information for themself as another  1180         # party.  1181   1182         if user == self.user:  1183             return  1184   1185         self.acquire_lock()  1186         try:  1187             freebusy = self.store.get_freebusy_for_other_for_update(self.user, user)  1188             fn(freebusy, user, for_organiser, True)  1189   1190             # Tidy up any obsolete recurrences.  1191   1192             self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))  1193             self.store.set_freebusy_for_other(self.user, freebusy, user)  1194   1195         finally:  1196             self.release_lock()  1197   1198     def update_freebusy_from_organiser(self, organiser):  1199   1200         "For the current user, record free/busy information from 'organiser'."  1201   1202         self.update_freebusy_from_participant(organiser, True)  1203   1204     def update_freebusy_from_attendees(self, attendees):  1205   1206         "For the current user, record free/busy information from 'attendees'."  1207   1208         obj = self.get_stored_object_version()  1209   1210         if not obj or not self.have_new_object():  1211             return False  1212   1213         # Filter out unrecognised attendees.  1214   1215         attendees = set(attendees).intersection(uri_values(obj.get_values("ATTENDEE")))  1216   1217         for attendee in attendees:  1218             self.update_freebusy_from_participant(attendee, False)  1219   1220         return True  1221   1222     def remove_freebusy_from_organiser(self, organiser):  1223   1224         "For the current user, remove free/busy information from 'organiser'."  1225   1226         self.update_freebusy_from_participant(organiser, True, self.remove_freebusy_for_participant)  1227   1228     def remove_freebusy_from_attendees(self, attendees):  1229   1230         "For the current user, remove free/busy information from 'attendees'."  1231   1232         for attendee in attendees.keys():  1233             self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant)  1234   1235     # Convenience methods for updating free/busy details at the event level.  1236   1237     def update_event_in_freebusy(self, for_organiser=True):  1238   1239         """  1240         Update free/busy information when handling an object, doing so for the  1241         organiser of an event if 'for_organiser' is set to a true value.  1242         """  1243   1244         freebusy = self.store.get_freebusy_for_update(self.user)  1245   1246         # Obtain the attendance attributes for this user, if available.  1247   1248         self.update_freebusy_for_participant(freebusy, self.user, for_organiser)  1249   1250         # Remove original recurrence details replaced by additional  1251         # recurrences, as well as obsolete additional recurrences.  1252   1253         self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))  1254         self.store.set_freebusy(self.user, freebusy)  1255   1256         if self.publisher and self.is_sharing() and self.is_publishing():  1257             self.publisher.set_freebusy(self.user, freebusy)  1258   1259         # Update free/busy provider information if the event may recur  1260         # indefinitely.  1261   1262         if self.possibly_recurring_indefinitely():  1263             self.store.append_freebusy_provider(self.user, self.obj)  1264   1265         return True  1266   1267     def remove_event_from_freebusy(self):  1268   1269         "Remove free/busy information when handling an object."  1270   1271         freebusy = self.store.get_freebusy_for_update(self.user)  1272   1273         self.remove_from_freebusy(freebusy)  1274         self.remove_freebusy_for_recurrences(freebusy)  1275         self.store.set_freebusy(self.user, freebusy)  1276   1277         if self.publisher and self.is_sharing() and self.is_publishing():  1278             self.publisher.set_freebusy(self.user, freebusy)  1279   1280         # Update free/busy provider information if the event may recur  1281         # indefinitely.  1282   1283         if self.possibly_recurring_indefinitely():  1284             self.store.remove_freebusy_provider(self.user, self.obj)  1285   1286     def update_event_in_freebusy_offers(self):  1287   1288         "Update free/busy offers when handling an object."  1289   1290         freebusy = self.store.get_freebusy_offers_for_update(self.user)  1291   1292         # Obtain the attendance attributes for this user, if available.  1293   1294         self.update_freebusy_for_participant(freebusy, self.user, offer=True)  1295   1296         # Remove original recurrence details replaced by additional  1297         # recurrences, as well as obsolete additional recurrences.  1298   1299         self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))  1300         self.store.set_freebusy_offers(self.user, freebusy)  1301   1302         return True  1303   1304     def remove_event_from_freebusy_offers(self):  1305   1306         "Remove free/busy offers when handling an object."  1307   1308         freebusy = self.store.get_freebusy_offers_for_update(self.user)  1309   1310         self.remove_from_freebusy(freebusy)  1311         self.remove_freebusy_for_recurrences(freebusy)  1312         self.store.set_freebusy_offers(self.user, freebusy)  1313   1314         return True  1315   1316     # Convenience methods for removing counter-proposals and updating the  1317     # request queue.  1318   1319     def remove_request(self):  1320         return self.store.dequeue_request(self.user, self.uid, self.recurrenceid)  1321   1322     def remove_event(self):  1323         return self.store.remove_event(self.user, self.uid, self.recurrenceid)  1324   1325     def remove_counter(self, attendee):  1326         self.remove_counters([attendee])  1327   1328     def remove_counters(self, attendees):  1329         for attendee in attendees:  1330             self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid)  1331   1332         if not self.store.get_counters(self.user, self.uid, self.recurrenceid):  1333             self.store.dequeue_request(self.user, self.uid, self.recurrenceid)  1334   1335 # vim: tabstop=4 expandtab shiftwidth=4