imip-agent

Annotated imiptools/handlers/person_outgoing.py

1309:644b7e259059
2017-10-14 Paul Boddie Support BCC sending suppression so that routines requesting it can still be used with senders that will not support it, usually because there are no outgoing routing destinations for those senders.
paul@96 1
#!/usr/bin/env python
paul@96 2
paul@96 3
"""
paul@96 4
Handlers for a person for whom scheduling is performed, inspecting outgoing
paul@96 5
messages to obtain scheduling done externally.
paul@146 6
paul@1123 7
Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
paul@146 8
paul@146 9
This program is free software; you can redistribute it and/or modify it under
paul@146 10
the terms of the GNU General Public License as published by the Free Software
paul@146 11
Foundation; either version 3 of the License, or (at your option) any later
paul@146 12
version.
paul@146 13
paul@146 14
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 15
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 16
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 17
details.
paul@146 18
paul@146 19
You should have received a copy of the GNU General Public License along with
paul@146 20
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@96 21
"""
paul@96 22
paul@705 23
from imiptools.client import Client
paul@847 24
from imiptools.data import get_uri, uri_dict, uri_values
paul@418 25
from imiptools.handlers import Handler
paul@683 26
from imiptools.handlers.common import CommonEvent
paul@96 27
paul@725 28
class PersonHandler(CommonEvent, Handler):
paul@96 29
paul@96 30
    "Handling mechanisms specific to people."
paul@96 31
paul@729 32
    def set_identity(self, method):
paul@130 33
paul@140 34
        """
paul@729 35
        Set the current user for the current object in the context of the given
paul@729 36
        'method'. It is usually set when initialising the handler, using the
paul@729 37
        recipient details, but outgoing messages do not reference the recipient
paul@729 38
        in this way.
paul@140 39
        """
paul@130 40
paul@832 41
        if self.obj and not self.user:
paul@729 42
            from_organiser = method in self.organiser_methods
paul@832 43
            if from_organiser:
paul@832 44
                self.user = get_uri(self.obj.get_value("ORGANIZER"))
paul@832 45
paul@832 46
            # Since there may be many attendees in an attendee-provided outgoing
paul@832 47
            # message, because counter-proposals can have more than one
paul@832 48
            # attendee, the attendee originating from the calendar system is
paul@832 49
            # chosen.
paul@832 50
paul@832 51
            else:
paul@847 52
                self.user = self.get_sending_attendee()
paul@140 53
paul@684 54
    def _add(self):
paul@684 55
paul@684 56
        "Add a recurrence for the current object."
paul@684 57
paul@705 58
        if not Client.is_participating(self):
paul@705 59
            return False
paul@684 60
paul@832 61
        # Check for event using UID.
paul@684 62
paul@832 63
        if not self.have_new_object():
paul@684 64
            return False
paul@684 65
paul@684 66
        # Ignore unknown objects.
paul@684 67
paul@684 68
        if not self.get_stored_object_version():
paul@684 69
            return
paul@684 70
paul@684 71
        # Record the event as a recurrence of the parent object.
paul@684 72
paul@684 73
        self.update_recurrenceid()
paul@684 74
paul@733 75
        # Set the additional occurrence.
paul@733 76
paul@733 77
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@733 78
paul@859 79
        # Remove any previous cancellations involving this event.
paul@859 80
paul@859 81
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 82
paul@684 83
        # Update free/busy information.
paul@684 84
paul@684 85
        self.update_event_in_freebusy()
paul@684 86
paul@684 87
        return True
paul@684 88
paul@799 89
    def _record(self, from_organiser=True, counter=False):
paul@140 90
paul@420 91
        """
paul@420 92
        Record details from the current object given a message originating
paul@580 93
        from an organiser if 'from_organiser' is set to a true value.
paul@420 94
        """
paul@140 95
paul@705 96
        if not Client.is_participating(self):
paul@705 97
            return False
paul@468 98
paul@682 99
        # Check for a new event, tolerating not-strictly-new events if the
paul@682 100
        # attendee is responding.
paul@468 101
paul@682 102
        if not self.have_new_object(strict=from_organiser):
paul@99 103
            return False
paul@97 104
paul@268 105
        # Update the object.
paul@96 106
paul@268 107
        if from_organiser:
paul@334 108
paul@361 109
            # Set the complete event or an additional occurrence.
paul@334 110
paul@468 111
            self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@334 112
paul@381 113
            # Remove additional recurrences if handling a complete event.
paul@796 114
            # Also remove any previous cancellations involving this event.
paul@381 115
paul@381 116
            if not self.recurrenceid:
paul@468 117
                self.store.remove_recurrences(self.user, self.uid)
paul@796 118
                self.store.remove_cancellations(self.user, self.uid)
paul@796 119
            else:
paul@796 120
                self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@381 121
paul@268 122
        else:
paul@1125 123
            # Occurrences that are still part of a parent object are separated,
paul@1125 124
            # attendance information transferred, and the free/busy details
paul@1125 125
            # updated.
paul@1125 126
paul@1125 127
            if self.is_newly_separated_occurrence():
paul@1125 128
                self.make_separate_occurrence(for_organiser=not from_organiser)
paul@1123 129
paul@420 130
            # Obtain valid attendees, merging their attendance with the stored
paul@420 131
            # object.
paul@420 132
paul@1125 133
            else:
paul@1125 134
                attendees = self.require_attendees(from_organiser)
paul@1125 135
                self.merge_attendance(attendees)
paul@96 136
paul@160 137
        # Remove any associated request.
paul@160 138
paul@917 139
        if from_organiser or self.has_indicated_attendance():
paul@917 140
            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@809 141
        self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@160 142
paul@96 143
        # Update free/busy information.
paul@96 144
paul@799 145
        if not counter:
paul@799 146
            self.update_event_in_freebusy(from_organiser)
paul@799 147
paul@799 148
        # For countered proposals, record the offer in the resource's
paul@799 149
        # free/busy collection.
paul@799 150
paul@799 151
        else:
paul@799 152
            self.update_event_in_freebusy_offers()
paul@96 153
paul@96 154
        return True
paul@96 155
paul@697 156
    def _remove(self):
paul@140 157
paul@580 158
        """
paul@580 159
        Remove details from the current object given a message originating
paul@580 160
        from an organiser if 'from_organiser' is set to a true value.
paul@580 161
        """
paul@140 162
paul@705 163
        if not Client.is_participating(self):
paul@705 164
            return False
paul@468 165
paul@468 166
        # Check for event using UID.
paul@468 167
paul@468 168
        if not self.have_new_object():
paul@140 169
            return False
paul@140 170
paul@527 171
        # Obtain any stored object, using parent object details if a newly-
paul@527 172
        # indicated occurrence is referenced.
paul@312 173
paul@606 174
        obj = self.get_stored_object_version()
paul@527 175
        old = not obj and self.get_parent_object() or obj
paul@527 176
paul@527 177
        if not old:
paul@420 178
            return False
paul@261 179
paul@527 180
        # Only cancel the event completely if all attendees are given.
paul@527 181
paul@527 182
        attendees = uri_dict(old.get_value_map("ATTENDEE"))
paul@312 183
        all_attendees = set(attendees.keys())
paul@312 184
        given_attendees = set(uri_values(self.obj.get_values("ATTENDEE")))
paul@690 185
        cancel_entire_event = not all_attendees.difference(given_attendees)
paul@312 186
paul@697 187
        # Update the recipient's record of the organiser's schedule.
paul@697 188
paul@697 189
        self.remove_freebusy_from_organiser(self.obj.get_value("ORGANIZER"))
paul@697 190
paul@312 191
        # Otherwise, remove the given attendees and update the event.
paul@312 192
paul@694 193
        if not cancel_entire_event and obj:
paul@312 194
            for attendee in given_attendees:
paul@314 195
                if attendees.has_key(attendee):
paul@314 196
                    del attendees[attendee]
paul@312 197
            obj["ATTENDEE"] = attendees.items()
paul@264 198
paul@312 199
        # Update the stored object with sequence information.
paul@312 200
paul@527 201
        if obj:
paul@577 202
            obj["SEQUENCE"] = self.obj.get_items("SEQUENCE") or []
paul@577 203
            obj["DTSTAMP"] = self.obj.get_items("DTSTAMP") or []
paul@312 204
paul@672 205
        # Update free/busy information.
paul@672 206
paul@672 207
        if cancel_entire_event or self.user in given_attendees:
paul@672 208
            self.remove_event_from_freebusy()
paul@911 209
            self.remove_freebusy_from_attendees(attendees)
paul@672 210
paul@527 211
        # Set the complete event if not an additional occurrence. For any newly-
paul@527 212
        # indicated occurrence, use the received event details.
paul@334 213
paul@527 214
        self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node())
paul@264 215
paul@694 216
        # Perform any cancellation after recording the latest state of the
paul@694 217
        # event.
paul@694 218
paul@694 219
        if cancel_entire_event:
paul@694 220
            self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@694 221
paul@261 222
        # Remove any associated request.
paul@261 223
paul@468 224
        self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
paul@809 225
        self.store.remove_counters(self.user, self.uid, self.recurrenceid)
paul@261 226
paul@140 227
        return True
paul@140 228
paul@804 229
    def _declinecounter(self):
paul@804 230
paul@804 231
        "Remove any counter-proposals for the given event."
paul@804 232
paul@804 233
        if not Client.is_participating(self):
paul@804 234
            return False
paul@804 235
paul@804 236
        # Check for event using UID.
paul@804 237
paul@804 238
        if not self.have_new_object():
paul@804 239
            return False
paul@804 240
paul@809 241
        self.remove_counters(uri_values(self.obj.get_values("ATTENDEE")))
paul@804 242
paul@96 243
class Event(PersonHandler):
paul@96 244
paul@96 245
    "An event handler."
paul@96 246
paul@96 247
    def add(self):
paul@686 248
paul@686 249
        "Record the addition of a recurrence to an event."
paul@686 250
paul@684 251
        self._add()
paul@96 252
paul@96 253
    def cancel(self):
paul@686 254
paul@686 255
        "Remove an event or a recurrence."
paul@686 256
paul@697 257
        self._remove()
paul@96 258
paul@96 259
    def counter(self):
paul@686 260
paul@799 261
        "Record an offer made by a counter-proposal."
paul@686 262
paul@799 263
        self._record(False, True)
paul@96 264
paul@96 265
    def declinecounter(self):
paul@686 266
paul@804 267
        "Expire any offer made by a counter-proposal."
paul@686 268
paul@804 269
        self._declinecounter()
paul@96 270
paul@96 271
    def publish(self):
paul@686 272
paul@686 273
        "Published events are recorded."
paul@686 274
paul@580 275
        self._record(True)
paul@96 276
paul@96 277
    def refresh(self):
paul@686 278
paul@686 279
        "Requests to refresh events do not provide event information."
paul@686 280
paul@353 281
        pass
paul@96 282
paul@96 283
    def reply(self):
paul@686 284
paul@686 285
        "Replies to requests are inspected for attendee information."
paul@686 286
paul@580 287
        self._record(False)
paul@96 288
paul@96 289
    def request(self):
paul@686 290
paul@686 291
        "Record events sent for potential scheduling."
paul@686 292
paul@580 293
        self._record(True)
paul@96 294
paul@96 295
# Handler registry.
paul@96 296
paul@96 297
handlers = [
paul@96 298
    ("VEVENT",      Event),
paul@96 299
    ]
paul@96 300
paul@96 301
# vim: tabstop=4 expandtab shiftwidth=4