imip-agent

imiptools/content.py

274:525ea9b0310f
2015-02-05 Paul Boddie Removed redundant next_date import.
     1 #!/usr/bin/env python     2      3 """     4 Interpretation and preparation of iMIP content, together with a content handling     5 mechanism employed by specific recipients.     6      7 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     8      9 This program is free software; you can redistribute it and/or modify it under    10 the terms of the GNU General Public License as published by the Free Software    11 Foundation; either version 3 of the License, or (at your option) any later    12 version.    13     14 This program is distributed in the hope that it will be useful, but WITHOUT    15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    16 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    17 details.    18     19 You should have received a copy of the GNU General Public License along with    20 this program.  If not, see <http://www.gnu.org/licenses/>.    21 """    22     23 from datetime import datetime, timedelta    24 from email.mime.text import MIMEText    25 from imiptools.config import MANAGER_PATH, MANAGER_URL    26 from imiptools.data import Object, parse_object, \    27                            get_address, get_uri, get_value, \    28                            is_new_object, uri_dict, uri_item    29 from imiptools.dates import format_datetime, to_timezone    30 from imiptools.period import can_schedule, insert_period, remove_period, \    31                              remove_from_freebusy, \    32                              remove_from_freebusy_for_other, \    33                              update_freebusy, update_freebusy_for_other    34 from pytz import timezone    35 from socket import gethostname    36 import imip_store    37     38 try:    39     from cStringIO import StringIO    40 except ImportError:    41     from StringIO import StringIO    42     43 # Handler mechanism objects.    44     45 def handle_itip_part(part, handlers):    46     47     """    48     Handle the given iTIP 'part' using the given 'handlers' dictionary.    49     50     Return a list of responses, each response being a tuple of the form    51     (outgoing-recipients, message-part).    52     """    53     54     method = part.get_param("method")    55     56     # Decode the data and parse it.    57     58     f = StringIO(part.get_payload(decode=True))    59     60     itip = parse_object(f, part.get_content_charset(), "VCALENDAR")    61     62     # Ignore the part if not a calendar object.    63     64     if not itip:    65         return    66     67     # Require consistency between declared and employed methods.    68     69     if get_value(itip, "METHOD") == method:    70     71         # Look for different kinds of sections.    72     73         all_results = []    74     75         for name, items in itip.items():    76     77             # Get a handler for the given section.    78     79             handler = handlers.get(name)    80             if not handler:    81                 continue    82     83             for item in items:    84     85                 # Dispatch to a handler and obtain any response.    86     87                 handler.set_object(Object({name : item}))    88                 methods[method](handler)()    89     90 # References to the Web interface.    91     92 def get_manager_url():    93     url_base = MANAGER_URL or "http://%s/" % gethostname()    94     return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/"))    95     96 def get_object_url(uid):    97     return "%s/%s" % (get_manager_url().rstrip("/"), uid)    98     99 class Handler:   100    101     "General handler support."   102    103     def __init__(self, senders=None, recipient=None, messenger=None):   104    105         """   106         Initialise the handler with the calendar 'obj' and the 'senders' and   107         'recipient' of the object (if specifically indicated).   108         """   109    110         self.senders = senders and set(map(get_address, senders))   111         self.recipient = recipient and get_address(recipient)   112         self.messenger = messenger   113    114         self.results = []   115         self.outgoing_methods = set()   116    117         self.obj = None   118         self.uid = None   119         self.sequence = None   120         self.dtstamp = None   121    122         self.store = imip_store.FileStore()   123    124         try:   125             self.publisher = imip_store.FilePublisher()   126         except OSError:   127             self.publisher = None   128    129     def set_object(self, obj):   130         self.obj = obj   131         self.uid = self.obj.get_value("UID")   132         self.sequence = self.obj.get_value("SEQUENCE")   133         self.dtstamp = self.obj.get_value("DTSTAMP")   134    135     def wrap(self, text, link=True):   136    137         "Wrap any valid message for passing to the recipient."   138    139         texts = []   140         texts.append(text)   141         if link:   142             texts.append("If your mail program cannot handle this "   143                          "message, you may view the details here:\n\n%s" %   144                          get_object_url(self.uid))   145    146         return self.add_result(None, None, MIMEText("\n".join(texts)))   147    148     # Result registration.   149    150     def add_result(self, method, outgoing_recipients, part):   151    152         """   153         Record a result having the given 'method', 'outgoing_recipients' and   154         message part.   155         """   156    157         if outgoing_recipients:   158             self.outgoing_methods.add(method)   159         self.results.append((outgoing_recipients, part))   160    161     def get_results(self):   162         return self.results   163    164     def get_outgoing_methods(self):   165         return self.outgoing_methods   166    167     # Access to calendar structures and other data.   168    169     def remove_from_freebusy(self, freebusy, attendee):   170         remove_from_freebusy(freebusy, attendee, self.uid, self.store)   171    172     def remove_from_freebusy_for_other(self, freebusy, user, other):   173         remove_from_freebusy_for_other(freebusy, user, other, self.uid, self.store)   174    175     def update_freebusy(self, freebusy, attendee, periods):   176         return update_freebusy(freebusy, attendee, periods, self.obj.get_value("TRANSP"), self.uid, self.store)   177    178     def update_freebusy_from_organiser(self, attendee, organiser_item):   179    180         """   181         For the 'attendee', record free/busy information from the   182         'organiser_item' (a value plus attributes).   183         """   184    185         organiser, organiser_attr = organiser_item   186    187         if organiser != attendee:   188             freebusy = self.store.get_freebusy_for_other(attendee, organiser)   189    190             if organiser_attr.get("PARTSTAT") != "DECLINED":   191                 update_freebusy_for_other(freebusy, attendee, organiser,   192                     self.obj.get_periods(), self.obj.get_value("TRANSP"),   193                     self.uid, self.store)   194             else:   195                 self.remove_from_freebusy_for_other(freebusy, attendee, organiser)   196    197     def update_freebusy_from_attendees(self, organiser, attendees):   198    199         "For the 'organiser', record free/busy information from 'attendees'."   200    201         for attendee, attendee_attr in attendees.items():   202             if organiser != attendee:   203                 freebusy = self.store.get_freebusy_for_other(organiser, attendee)   204    205                 if attendee_attr.get("PARTSTAT") != "DECLINED":   206                     update_freebusy_for_other(freebusy, organiser, attendee,   207                         self.obj.get_periods(), self.obj.get_value("TRANSP"),   208                         self.uid, self.store)   209                 else:   210                     self.remove_from_freebusy_for_other(freebusy, organiser, attendee)   211    212     def can_schedule(self, freebusy, periods):   213         return can_schedule(freebusy, periods, self.uid)   214    215     def filter_by_senders(self, mapping):   216    217         """   218         Return a list of items from 'mapping' filtered using sender information.   219         """   220    221         if self.senders:   222    223             # Get a mapping from senders to identities.   224    225             identities = self.get_sender_identities(mapping)   226    227             # Find the senders that are valid.   228    229             senders = map(get_address, identities)   230             valid = self.senders.intersection(senders)   231    232             # Return the true identities.   233    234             return [identities[get_uri(address)] for address in valid]   235         else:   236             return mapping   237    238     def filter_by_recipient(self, mapping):   239    240         """   241         Return a list of items from 'mapping' filtered using recipient   242         information.   243         """   244    245         if self.recipient:   246             addresses = set(map(get_address, mapping))   247             return map(get_uri, addresses.intersection([self.recipient]))   248         else:   249             return mapping   250    251     def require_organiser_and_attendees(self, from_organiser=True):   252    253         """   254         Return the organiser and attendees for the current object, filtered for   255         the recipient of interest. Return None if no identities are eligible.   256    257         Organiser and attendee identities are provided as lower case values.   258         """   259    260         attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))   261         organiser_item = uri_item(self.obj.get_item("ORGANIZER"))   262    263         # Only provide details for attendees who sent/receive the message.   264    265         attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders   266    267         attendees = {}   268         for attendee in attendee_filter_fn(attendee_map):   269             attendees[attendee] = attendee_map[attendee]   270    271         if not attendees or not organiser_item:   272             return None   273    274         # Only provide details for an organiser who sent/receives the message.   275    276         organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient   277    278         if not organiser_filter_fn(dict([organiser_item])):   279             return None   280    281         return organiser_item, attendees   282    283     def get_sender_identities(self, mapping):   284    285         """   286         Return a mapping from actual senders to the identities for which they   287         have provided data, extracting this information from the given   288         'mapping'.   289         """   290    291         senders = {}   292    293         for value, attr in mapping.items():   294             sent_by = attr.get("SENT-BY")   295             if sent_by:   296                 senders[get_uri(sent_by)] = value   297             else:   298                 senders[value] = value   299    300         return senders   301    302     def get_object(self, user):   303    304         """   305         Return the stored object to which the current object refers for the   306         given 'user' and for the given 'objtype'.   307         """   308    309         f = self.store.get_event(user, self.uid)   310         fragment = f and parse_object(f, "utf-8")   311         return fragment and Object(fragment)   312    313     def have_new_object(self, attendee, obj=None):   314    315         """   316         Return whether the current object is new to the 'attendee' (or if the   317         given 'obj' is new).   318         """   319    320         obj = obj or self.get_object(attendee)   321    322         # If found, compare SEQUENCE and potentially DTSTAMP.   323    324         if obj:   325             sequence = obj.get_value("SEQUENCE")   326             dtstamp = obj.get_value("DTSTAMP")   327    328             # If the request refers to an older version of the object, ignore   329             # it.   330    331             return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp,   332                 self.is_partstat_updated(obj))   333    334         return True   335    336     def is_partstat_updated(self, obj):   337    338         """   339         Return whether the participant status has been updated in the current   340         object in comparison to the given 'obj'.   341    342         NOTE: Some clients like Claws Mail erase time information from DTSTAMP   343         NOTE: and make it invalid. Thus, such attendance information may also be   344         NOTE: incorporated into any new object assessment.   345         """   346    347         old_attendees = obj.get_value_map("ATTENDEE")   348         new_attendees = self.obj.get_value_map("ATTENDEE")   349    350         for attendee, attr in old_attendees.items():   351             old_partstat = attr.get("PARTSTAT")   352             new_attr = new_attendees.get(attendee)   353             new_partstat = new_attr and new_attr.get("PARTSTAT")   354    355             if old_partstat == "NEEDS-ACTION" and new_partstat and \   356                new_partstat != old_partstat:   357    358                 return True   359    360         return False   361    362     def merge_attendance(self, attendees, identity):   363    364         """   365         Merge attendance from the current object's 'attendees' into the version   366         stored for the given 'identity'.   367         """   368    369         obj = self.get_object(identity)   370    371         if not obj or not self.have_new_object(identity, obj=obj):   372             return False   373    374         # Get attendee details in a usable form.   375    376         attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))   377    378         for attendee, attendee_attr in attendees.items():   379    380             # Update attendance in the loaded object.   381    382             attendee_map[attendee] = attendee_attr   383    384         # Set the new details and store the object.   385    386         obj["ATTENDEE"] = attendee_map.items()   387    388         self.store.set_event(identity, self.uid, obj.to_node())   389    390         return True   391    392     def update_dtstamp(self):   393    394         "Update the DTSTAMP in the current object."   395    396         dtstamp = self.obj.get_utc_datetime("DTSTAMP")   397         utcnow = to_timezone(datetime.utcnow(), "UTC")   398         self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})]   399    400     def set_sequence(self, increment=False):   401    402         "Update the SEQUENCE in the current object."   403    404         sequence = self.obj.get_value("SEQUENCE") or "0"   405         self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]   406    407 # Handler registry.   408    409 methods = {   410     "ADD"            : lambda handler: handler.add,   411     "CANCEL"         : lambda handler: handler.cancel,   412     "COUNTER"        : lambda handler: handler.counter,   413     "DECLINECOUNTER" : lambda handler: handler.declinecounter,   414     "PUBLISH"        : lambda handler: handler.publish,   415     "REFRESH"        : lambda handler: handler.refresh,   416     "REPLY"          : lambda handler: handler.reply,   417     "REQUEST"        : lambda handler: handler.request,   418     }   419    420 # vim: tabstop=4 expandtab shiftwidth=4