1.1 --- a/imiptools/content.py Sun Mar 22 18:36:34 2015 +0100
1.2 +++ b/imiptools/content.py Sun Mar 22 18:37:13 2015 +0100
1.3 @@ -20,19 +20,7 @@
1.4 this program. If not, see <http://www.gnu.org/licenses/>.
1.5 """
1.6
1.7 -from datetime import datetime, timedelta
1.8 -from email.mime.text import MIMEText
1.9 -from imiptools.config import MANAGER_PATH, MANAGER_URL
1.10 -from imiptools.data import Object, parse_object, \
1.11 - get_address, get_uri, get_value, get_window_end, \
1.12 - is_new_object, uri_dict, uri_item, uri_values
1.13 -from imiptools.dates import format_datetime, get_default_timezone, to_timezone
1.14 -from imiptools.period import can_schedule, insert_period, remove_period, \
1.15 - remove_additional_periods, remove_affected_period, \
1.16 - update_freebusy
1.17 -from imiptools.profile import Preferences
1.18 -from socket import gethostname
1.19 -import imip_store
1.20 +from imiptools.data import Object, parse_object, get_value
1.21
1.22 try:
1.23 from cStringIO import StringIO
1.24 @@ -86,458 +74,6 @@
1.25 handler.set_object(Object({name : item}))
1.26 methods[method](handler)()
1.27
1.28 -# References to the Web interface.
1.29 -
1.30 -def get_manager_url():
1.31 - url_base = MANAGER_URL or "http://%s/" % gethostname()
1.32 - return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/"))
1.33 -
1.34 -def get_object_url(uid, recurrenceid=None):
1.35 - return "%s/%s%s" % (
1.36 - get_manager_url().rstrip("/"), uid,
1.37 - recurrenceid and "/%s" % recurrenceid or ""
1.38 - )
1.39 -
1.40 -class Handler:
1.41 -
1.42 - "General handler support."
1.43 -
1.44 - def __init__(self, senders=None, recipient=None, messenger=None):
1.45 -
1.46 - """
1.47 - Initialise the handler with the calendar 'obj' and the 'senders' and
1.48 - 'recipient' of the object (if specifically indicated).
1.49 - """
1.50 -
1.51 - self.senders = senders and set(map(get_address, senders))
1.52 - self.recipient = recipient and get_address(recipient)
1.53 - self.messenger = messenger
1.54 -
1.55 - self.results = []
1.56 - self.outgoing_methods = set()
1.57 -
1.58 - self.obj = None
1.59 - self.uid = None
1.60 - self.recurrenceid = None
1.61 - self.sequence = None
1.62 - self.dtstamp = None
1.63 -
1.64 - self.store = imip_store.FileStore()
1.65 -
1.66 - try:
1.67 - self.publisher = imip_store.FilePublisher()
1.68 - except OSError:
1.69 - self.publisher = None
1.70 -
1.71 - def set_object(self, obj):
1.72 - self.obj = obj
1.73 - self.uid = self.obj.get_value("UID")
1.74 - self.recurrenceid = format_datetime(self.obj.get_utc_datetime("RECURRENCE-ID"))
1.75 - self.sequence = self.obj.get_value("SEQUENCE")
1.76 - self.dtstamp = self.obj.get_value("DTSTAMP")
1.77 -
1.78 - def wrap(self, text, link=True):
1.79 -
1.80 - "Wrap any valid message for passing to the recipient."
1.81 -
1.82 - texts = []
1.83 - texts.append(text)
1.84 - if link:
1.85 - texts.append("If your mail program cannot handle this "
1.86 - "message, you may view the details here:\n\n%s" %
1.87 - get_object_url(self.uid, self.recurrenceid))
1.88 -
1.89 - return self.add_result(None, None, MIMEText("\n".join(texts)))
1.90 -
1.91 - # Result registration.
1.92 -
1.93 - def add_result(self, method, outgoing_recipients, part):
1.94 -
1.95 - """
1.96 - Record a result having the given 'method', 'outgoing_recipients' and
1.97 - message part.
1.98 - """
1.99 -
1.100 - if outgoing_recipients:
1.101 - self.outgoing_methods.add(method)
1.102 - self.results.append((outgoing_recipients, part))
1.103 -
1.104 - def get_results(self):
1.105 - return self.results
1.106 -
1.107 - def get_outgoing_methods(self):
1.108 - return self.outgoing_methods
1.109 -
1.110 - # Convenience methods for modifying free/busy collections.
1.111 -
1.112 - def remove_from_freebusy(self, freebusy):
1.113 -
1.114 - "Remove this event from the given 'freebusy' collection."
1.115 -
1.116 - remove_period(freebusy, self.uid, self.recurrenceid)
1.117 -
1.118 - def remove_freebusy_for_recurrences(self, freebusy, recurrenceids=None):
1.119 -
1.120 - """
1.121 - Remove from 'freebusy' any original recurrence from parent free/busy
1.122 - details for the current object, if the current object is a specific
1.123 - additional recurrence. Otherwise, remove all additional recurrence
1.124 - information corresponding to 'recurrenceids', or if omitted, all
1.125 - recurrences.
1.126 - """
1.127 -
1.128 - if self.recurrenceid:
1.129 - remove_affected_period(freebusy, self.uid, self.recurrenceid)
1.130 - else:
1.131 - # Remove obsolete recurrence periods.
1.132 -
1.133 - remove_additional_periods(freebusy, self.uid, recurrenceids)
1.134 -
1.135 - # Remove original periods affected by additional recurrences.
1.136 -
1.137 - if recurrenceids:
1.138 - for recurrenceid in recurrenceids:
1.139 - remove_affected_period(freebusy, self.uid, recurrenceid)
1.140 -
1.141 - def _update_freebusy(self, freebusy, periods, recurrenceid, transp=None):
1.142 -
1.143 - """
1.144 - Update the 'freebusy' collection with the given 'periods', indicating an
1.145 - explicit 'recurrenceid' to affect either a recurrence or the parent
1.146 - event.
1.147 - """
1.148 -
1.149 - update_freebusy(freebusy, periods,
1.150 - transp or self.obj.get_value("TRANSP"),
1.151 - self.uid, recurrenceid,
1.152 - self.obj.get_value("SUMMARY"),
1.153 - self.obj.get_value("ORGANIZER"))
1.154 -
1.155 - def update_freebusy(self, freebusy, periods, transp=None):
1.156 -
1.157 - """
1.158 - Update the 'freebusy' collection for this event with the given
1.159 - 'periods'.
1.160 - """
1.161 -
1.162 - self._update_freebusy(freebusy, periods, self.recurrenceid, transp)
1.163 -
1.164 - def update_freebusy_for_participant(self, freebusy, periods, attr, for_organiser=False):
1.165 -
1.166 - """
1.167 - Update the 'freebusy' collection using the given 'periods', subject to
1.168 - the 'attr' provided for the participant, indicating whether this is
1.169 - being generated 'for_organiser' or not.
1.170 - """
1.171 -
1.172 - # Organisers employ a special transparency.
1.173 -
1.174 - if for_organiser or attr.get("PARTSTAT") != "DECLINED":
1.175 - self.update_freebusy(freebusy, periods, transp=(for_organiser and "ORG" or None))
1.176 - else:
1.177 - self.remove_from_freebusy(freebusy)
1.178 -
1.179 - # Convenience methods for updating stored free/busy information.
1.180 -
1.181 - def update_freebusy_from_participant(self, user, participant_item, for_organiser):
1.182 -
1.183 - """
1.184 - For the given 'user', record the free/busy information for the
1.185 - 'participant_item' (a value plus attributes) representing a different
1.186 - identity, thus maintaining a separate record of their free/busy details.
1.187 - """
1.188 -
1.189 - participant, participant_attr = participant_item
1.190 -
1.191 - if participant == user:
1.192 - return
1.193 -
1.194 - freebusy = self.store.get_freebusy_for_other(user, participant)
1.195 - tzid = self.get_tzid(user)
1.196 - window_end = get_window_end(tzid)
1.197 - periods = self.obj.get_periods_for_freebusy(tzid, window_end)
1.198 -
1.199 - # Record in the free/busy details unless a non-participating attendee.
1.200 -
1.201 - self.update_freebusy_for_participant(freebusy, periods, participant_attr,
1.202 - for_organiser and self.is_not_attendee(participant, self.obj))
1.203 -
1.204 - self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(user, self.uid))
1.205 - self.store.set_freebusy_for_other(user, freebusy, participant)
1.206 -
1.207 - def update_freebusy_from_organiser(self, attendee, organiser_item):
1.208 -
1.209 - """
1.210 - For the 'attendee', record free/busy information from the
1.211 - 'organiser_item' (a value plus attributes).
1.212 - """
1.213 -
1.214 - self.update_freebusy_from_participant(attendee, organiser_item, True)
1.215 -
1.216 - def update_freebusy_from_attendees(self, organiser, attendees):
1.217 -
1.218 - "For the 'organiser', record free/busy information from 'attendees'."
1.219 -
1.220 - for attendee_item in attendees.items():
1.221 - self.update_freebusy_from_participant(organiser, attendee_item, False)
1.222 -
1.223 - # Logic, filtering and access to calendar structures and other data.
1.224 -
1.225 - def is_not_attendee(self, identity, obj):
1.226 -
1.227 - "Return whether 'identity' is not an attendee in 'obj'."
1.228 -
1.229 - return identity not in uri_values(obj.get_values("ATTENDEE"))
1.230 -
1.231 - def can_schedule(self, freebusy, periods):
1.232 - return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
1.233 -
1.234 - def filter_by_senders(self, mapping):
1.235 -
1.236 - """
1.237 - Return a list of items from 'mapping' filtered using sender information.
1.238 - """
1.239 -
1.240 - if self.senders:
1.241 -
1.242 - # Get a mapping from senders to identities.
1.243 -
1.244 - identities = self.get_sender_identities(mapping)
1.245 -
1.246 - # Find the senders that are valid.
1.247 -
1.248 - senders = map(get_address, identities)
1.249 - valid = self.senders.intersection(senders)
1.250 -
1.251 - # Return the true identities.
1.252 -
1.253 - return [identities[get_uri(address)] for address in valid]
1.254 - else:
1.255 - return mapping
1.256 -
1.257 - def filter_by_recipient(self, mapping):
1.258 -
1.259 - """
1.260 - Return a list of items from 'mapping' filtered using recipient
1.261 - information.
1.262 - """
1.263 -
1.264 - if self.recipient:
1.265 - addresses = set(map(get_address, mapping))
1.266 - return map(get_uri, addresses.intersection([self.recipient]))
1.267 - else:
1.268 - return mapping
1.269 -
1.270 - def require_organiser(self, from_organiser=True):
1.271 -
1.272 - """
1.273 - Return the organiser for the current object, filtered for the sender or
1.274 - recipient of interest. Return None if no identities are eligible.
1.275 -
1.276 - The organiser identity is normalized.
1.277 - """
1.278 -
1.279 - organiser_item = uri_item(self.obj.get_item("ORGANIZER"))
1.280 -
1.281 - # Only provide details for an organiser who sent/receives the message.
1.282 -
1.283 - organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient
1.284 -
1.285 - if not organiser_filter_fn(dict([organiser_item])):
1.286 - return None
1.287 -
1.288 - return organiser_item
1.289 -
1.290 - def require_attendees(self, from_organiser=True):
1.291 -
1.292 - """
1.293 - Return the attendees for the current object, filtered for the sender or
1.294 - recipient of interest. Return None if no identities are eligible.
1.295 -
1.296 - The attendee identities are normalized.
1.297 - """
1.298 -
1.299 - attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
1.300 -
1.301 - # Only provide details for attendees who sent/receive the message.
1.302 -
1.303 - attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders
1.304 -
1.305 - attendees = {}
1.306 - for attendee in attendee_filter_fn(attendee_map):
1.307 - attendees[attendee] = attendee_map[attendee]
1.308 -
1.309 - return attendees
1.310 -
1.311 - def require_organiser_and_attendees(self, from_organiser=True):
1.312 -
1.313 - """
1.314 - Return the organiser and attendees for the current object, filtered for
1.315 - the recipient of interest. Return None if no identities are eligible.
1.316 -
1.317 - Organiser and attendee identities are normalized.
1.318 - """
1.319 -
1.320 - organiser_item = self.require_organiser(from_organiser)
1.321 - attendees = self.require_attendees(from_organiser)
1.322 -
1.323 - if not attendees or not organiser_item:
1.324 - return None
1.325 -
1.326 - return organiser_item, attendees
1.327 -
1.328 - def get_sender_identities(self, mapping):
1.329 -
1.330 - """
1.331 - Return a mapping from actual senders to the identities for which they
1.332 - have provided data, extracting this information from the given
1.333 - 'mapping'.
1.334 - """
1.335 -
1.336 - senders = {}
1.337 -
1.338 - for value, attr in mapping.items():
1.339 - sent_by = attr.get("SENT-BY")
1.340 - if sent_by:
1.341 - senders[get_uri(sent_by)] = value
1.342 - else:
1.343 - senders[value] = value
1.344 -
1.345 - return senders
1.346 -
1.347 - def _get_object(self, user, uid, recurrenceid):
1.348 -
1.349 - """
1.350 - Return the stored object for the given 'user', 'uid' and 'recurrenceid'.
1.351 - """
1.352 -
1.353 - fragment = self.store.get_event(user, uid, recurrenceid)
1.354 - return fragment and Object(fragment)
1.355 -
1.356 - def get_object(self, user):
1.357 -
1.358 - """
1.359 - Return the stored object to which the current object refers for the
1.360 - given 'user'.
1.361 - """
1.362 -
1.363 - return self._get_object(user, self.uid, self.recurrenceid)
1.364 -
1.365 - def get_parent_object(self, user):
1.366 -
1.367 - """
1.368 - Return the parent object to which the current object refers for the
1.369 - given 'user'.
1.370 - """
1.371 -
1.372 - return self.recurrenceid and self._get_object(user, self.uid, None) or None
1.373 -
1.374 - def have_new_object(self, attendee, obj=None):
1.375 -
1.376 - """
1.377 - Return whether the current object is new to the 'attendee' (or if the
1.378 - given 'obj' is new).
1.379 - """
1.380 -
1.381 - obj = obj or self.get_object(attendee)
1.382 -
1.383 - # If found, compare SEQUENCE and potentially DTSTAMP.
1.384 -
1.385 - if obj:
1.386 - sequence = obj.get_value("SEQUENCE")
1.387 - dtstamp = obj.get_value("DTSTAMP")
1.388 -
1.389 - # If the request refers to an older version of the object, ignore
1.390 - # it.
1.391 -
1.392 - return is_new_object(sequence, self.sequence, dtstamp, self.dtstamp,
1.393 - self.is_partstat_updated(obj))
1.394 -
1.395 - return True
1.396 -
1.397 - def is_partstat_updated(self, obj):
1.398 -
1.399 - """
1.400 - Return whether the participant status has been updated in the current
1.401 - object in comparison to the given 'obj'.
1.402 -
1.403 - NOTE: Some clients like Claws Mail erase time information from DTSTAMP
1.404 - NOTE: and make it invalid. Thus, such attendance information may also be
1.405 - NOTE: incorporated into any new object assessment.
1.406 - """
1.407 -
1.408 - old_attendees = uri_dict(obj.get_value_map("ATTENDEE"))
1.409 - new_attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
1.410 -
1.411 - for attendee, attr in old_attendees.items():
1.412 - old_partstat = attr.get("PARTSTAT")
1.413 - new_attr = new_attendees.get(attendee)
1.414 - new_partstat = new_attr and new_attr.get("PARTSTAT")
1.415 -
1.416 - if old_partstat == "NEEDS-ACTION" and new_partstat and \
1.417 - new_partstat != old_partstat:
1.418 -
1.419 - return True
1.420 -
1.421 - return False
1.422 -
1.423 - def merge_attendance(self, attendees, identity):
1.424 -
1.425 - """
1.426 - Merge attendance from the current object's 'attendees' into the version
1.427 - stored for the given 'identity'.
1.428 - """
1.429 -
1.430 - obj = self.get_object(identity)
1.431 -
1.432 - if not obj or not self.have_new_object(identity, obj=obj):
1.433 - return False
1.434 -
1.435 - # Get attendee details in a usable form.
1.436 -
1.437 - attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
1.438 -
1.439 - for attendee, attendee_attr in attendees.items():
1.440 -
1.441 - # Update attendance in the loaded object.
1.442 -
1.443 - attendee_map[attendee] = attendee_attr
1.444 -
1.445 - # Set the new details and store the object.
1.446 -
1.447 - obj["ATTENDEE"] = attendee_map.items()
1.448 -
1.449 - # Set the complete event if not an additional occurrence.
1.450 -
1.451 - event = obj.to_node()
1.452 - recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
1.453 -
1.454 - self.store.set_event(identity, self.uid, self.recurrenceid, event)
1.455 -
1.456 - return True
1.457 -
1.458 - def update_dtstamp(self):
1.459 -
1.460 - "Update the DTSTAMP in the current object."
1.461 -
1.462 - dtstamp = self.obj.get_utc_datetime("DTSTAMP")
1.463 - utcnow = to_timezone(datetime.utcnow(), "UTC")
1.464 - self.obj["DTSTAMP"] = [(format_datetime(dtstamp > utcnow and dtstamp or utcnow), {})]
1.465 -
1.466 - def set_sequence(self, increment=False):
1.467 -
1.468 - "Update the SEQUENCE in the current object."
1.469 -
1.470 - sequence = self.obj.get_value("SEQUENCE") or "0"
1.471 - self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]
1.472 -
1.473 - def get_tzid(self, identity):
1.474 -
1.475 - "Return the time regime applicable for the given 'identity'."
1.476 -
1.477 - preferences = Preferences(identity)
1.478 - return preferences.get("TZID") or get_default_timezone()
1.479 -
1.480 # Handler registry.
1.481
1.482 methods = {