imip-agent

Annotated imiptools/handlers/resource.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@48 1
#!/usr/bin/env python
paul@48 2
paul@48 3
"""
paul@48 4
Handlers for a resource.
paul@146 5
paul@1263 6
Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@48 20
"""
paul@48 21
paul@1153 22
from email.mime.text import MIMEText
paul@1176 23
from imiptools.data import get_address, uri_dict
paul@418 24
from imiptools.handlers import Handler
paul@683 25
from imiptools.handlers.common import CommonFreebusy, CommonEvent
paul@1039 26
from imiptools.handlers.scheduling import apply_scheduling_functions, \
paul@1040 27
                                          confirm_scheduling, \
paul@1040 28
                                          finish_scheduling, \
paul@1040 29
                                          retract_scheduling
paul@48 30
paul@725 31
class ResourceHandler(CommonEvent, Handler):
paul@131 32
paul@131 33
    "Handling mechanisms specific to resources."
paul@131 34
paul@937 35
    def _process(self, handle_for_attendee):
paul@131 36
paul@420 37
        """
paul@420 38
        Record details from the incoming message, using the given
paul@420 39
        'handle_for_attendee' callable to process any valid message
paul@420 40
        appropriately.
paul@420 41
        """
paul@420 42
paul@131 43
        oa = self.require_organiser_and_attendees()
paul@131 44
        if not oa:
paul@131 45
            return None
paul@131 46
paul@131 47
        organiser_item, attendees = oa
paul@131 48
paul@468 49
        # Process for the current user, a resource as attendee.
paul@131 50
paul@738 51
        if not self.have_new_object():
paul@468 52
            return None
paul@131 53
paul@468 54
        # Collect response objects produced when handling the request.
paul@131 55
paul@662 56
        handle_for_attendee()
paul@131 57
paul@676 58
    def _add_for_attendee(self):
paul@131 59
paul@420 60
        """
paul@676 61
        Attempt to add a recurrence to an existing object for the current user.
paul@676 62
        This does not request a response concerning participation, apparently.
paul@420 63
        """
paul@420 64
paul@737 65
        # Request details where configured, doing so for unknown objects anyway.
paul@676 66
paul@737 67
        if self.will_refresh():
paul@737 68
            self.make_refresh()
paul@676 69
            return
paul@676 70
paul@676 71
        # Record the event as a recurrence of the parent object.
paul@676 72
paul@676 73
        self.update_recurrenceid()
paul@737 74
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@676 75
paul@859 76
        # Remove any previous cancellations involving this event.
paul@859 77
paul@859 78
        self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@859 79
paul@676 80
        # Update free/busy information.
paul@676 81
paul@676 82
        self.update_event_in_freebusy(for_organiser=False)
paul@676 83
paul@1039 84
        # Confirm the scheduling of the recurrence.
paul@1039 85
paul@1039 86
        self.confirm_scheduling()
paul@1039 87
paul@676 88
    def _schedule_for_attendee(self):
paul@676 89
paul@676 90
        "Attempt to schedule the current object for the current user."
paul@676 91
paul@737 92
        attendee_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[self.user]
paul@1177 93
        delegates = None
paul@1040 94
paul@1040 95
        # Attempt to schedule the event.
paul@1040 96
paul@1191 97
        try:
paul@1191 98
            scheduled, description = self.schedule()
paul@655 99
paul@1040 100
            # Update the participation of the resource in the object.
paul@1040 101
            # Update free/busy information.
paul@662 102
paul@1040 103
            if scheduled in ("ACCEPTED", "DECLINED"):
paul@1040 104
                method = "REPLY"
paul@1040 105
                attendee_attr = self.update_participation(scheduled)
paul@662 106
paul@1040 107
                # Set the complete event or an additional occurrence.
paul@334 108
paul@1040 109
                event = self.obj.to_node()
paul@1040 110
                self.store.set_event(self.user, self.uid, self.recurrenceid, event)
paul@381 111
paul@1040 112
                # Remove additional recurrences if handling a complete event.
paul@1040 113
                # Also remove any previous cancellations involving this event.
paul@381 114
paul@1040 115
                if not self.recurrenceid:
paul@1040 116
                    self.store.remove_recurrences(self.user, self.uid)
paul@1040 117
                    self.store.remove_cancellations(self.user, self.uid)
paul@1040 118
                else:
paul@1040 119
                    self.store.remove_cancellation(self.user, self.uid, self.recurrenceid)
paul@361 120
paul@1263 121
                # Update free/busy details only after recurrences have been
paul@1263 122
                # removed, where appropriate.
paul@1263 123
paul@1263 124
                self.update_event_in_freebusy(for_organiser=False)
paul@1263 125
                self.remove_event_from_freebusy_offers()
paul@1263 126
paul@1040 127
                if scheduled == "ACCEPTED":
paul@1040 128
                    self.confirm_scheduling()
paul@1039 129
paul@1177 130
            # For delegated proposals, prepare a request to the delegates in
paul@1176 131
            # addition to the usual response.
paul@1176 132
paul@1176 133
            elif scheduled == "DELEGATED":
paul@1176 134
                method = "REPLY"
paul@1176 135
                attendee_attr = self.update_participation("DELEGATED")
paul@1176 136
paul@1176 137
                # The recipient will have indicated the delegate whose details
paul@1176 138
                # will have been added to the object.
paul@1176 139
paul@1177 140
                delegates = attendee_attr["DELEGATED-TO"]
paul@1176 141
paul@1040 142
            # For countered proposals, record the offer in the resource's
paul@1040 143
            # free/busy collection.
paul@1039 144
paul@1040 145
            elif scheduled == "COUNTER":
paul@1040 146
                method = "COUNTER"
paul@1040 147
                self.update_event_in_freebusy_offers()
paul@1040 148
paul@1040 149
            # For inappropriate periods, reply declining participation.
paul@936 150
paul@1040 151
            else:
paul@1040 152
                method = "REPLY"
paul@1040 153
                attendee_attr = self.update_participation("DECLINED")
paul@936 154
paul@1040 155
        # Confirm any scheduling.
paul@936 156
paul@1040 157
        finally:
paul@1040 158
            self.finish_scheduling()
paul@936 159
paul@1176 160
        # Determine the recipients of the outgoing messages.
paul@1176 161
paul@1153 162
        recipients = map(get_address, self.obj.get_values("ORGANIZER"))
paul@1153 163
paul@1153 164
        # Add any description of the scheduling decision.
paul@1153 165
paul@1153 166
        self.add_result(None, recipients, MIMEText(description))
paul@1153 167
paul@580 168
        # Make a version of the object with just this attendee, update the
paul@580 169
        # DTSTAMP in the response, and return the object for sending.
paul@574 170
paul@745 171
        self.update_sender(attendee_attr)
paul@1176 172
        attendees = [(self.user, attendee_attr)]
paul@1176 173
paul@1177 174
        # Add delegates if delegating (RFC 5546 being inconsistent here since
paul@1176 175
        # it provides an example reply to the organiser without the delegate).
paul@1176 176
paul@1177 177
        if delegates:
paul@1177 178
            for delegate in delegates:
paul@1177 179
                delegate_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegate]
paul@1177 180
                attendees.append((delegate, delegate_attr))
paul@1176 181
paul@1176 182
        # Reply to the delegator in addition to the organiser if replying to a
paul@1176 183
        # delegation request.
paul@1176 184
paul@1177 185
        delegators = self.is_delegation()
paul@1177 186
        if delegators:
paul@1177 187
            for delegator in delegators:
paul@1177 188
                delegator_attr = uri_dict(self.obj.get_value_map("ATTENDEE"))[delegator]
paul@1177 189
                attendees.append((delegator, delegator_attr))
paul@1177 190
                recipients.append(get_address(delegator))
paul@1176 191
paul@1176 192
        # Prepare the response for the organiser plus any delegator.
paul@1176 193
paul@1176 194
        self.obj["ATTENDEE"] = attendees
paul@574 195
        self.update_dtstamp()
paul@1176 196
        self.add_result(method, recipients, self.object_to_part(method, self.obj))
paul@1176 197
paul@1177 198
        # If delegating, send a request to the delegates.
paul@1176 199
paul@1177 200
        if delegates:
paul@1176 201
            method = "REQUEST"
paul@1177 202
            self.add_result(method, map(get_address, delegates), self.object_to_part(method, self.obj))
paul@131 203
paul@468 204
    def _cancel_for_attendee(self):
paul@131 205
paul@420 206
        """
paul@468 207
        Cancel for the current user their attendance of the event described by
paul@468 208
        the current object.
paul@420 209
        """
paul@420 210
paul@580 211
        # Update free/busy information.
paul@131 212
paul@580 213
        self.remove_event_from_freebusy()
paul@580 214
paul@672 215
        # Update the stored event and cancel it.
paul@672 216
paul@672 217
        self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
paul@672 218
        self.store.cancel_event(self.user, self.uid, self.recurrenceid)
paul@672 219
paul@1039 220
        # Retract the scheduling of the event.
paul@1039 221
paul@1039 222
        self.retract_scheduling()
paul@1039 223
paul@710 224
    def _revoke_for_attendee(self):
paul@710 225
paul@710 226
        "Revoke any counter-proposal recorded as a free/busy offer."
paul@710 227
paul@710 228
        self.remove_event_from_freebusy_offers()
paul@710 229
paul@936 230
    # Scheduling details.
paul@936 231
paul@1058 232
    def get_scheduling_functions(self):
paul@1058 233
paul@1058 234
        "Return the scheduling functions for the resource."
paul@1058 235
paul@1058 236
        return self.get_preferences().get("scheduling_function",
paul@1058 237
            "schedule_in_freebusy").split("\n")
paul@1058 238
paul@936 239
    def schedule(self):
paul@936 240
paul@936 241
        """
paul@936 242
        Attempt to schedule the current object, returning an indication of the
paul@936 243
        kind of response to be returned: "COUNTER" for counter-proposals,
paul@936 244
        "ACCEPTED" for acceptances, "DECLINED" for rejections, and None for
paul@936 245
        invalid requests.
paul@936 246
        """
paul@936 247
paul@1058 248
        return apply_scheduling_functions(self)
paul@936 249
paul@1039 250
    def confirm_scheduling(self):
paul@1039 251
paul@1039 252
        "Confirm that this event has been scheduled."
paul@1039 253
paul@1058 254
        confirm_scheduling(self)
paul@1039 255
paul@1040 256
    def finish_scheduling(self):
paul@1040 257
paul@1040 258
        "Finish the scheduling, unlocking resources where appropriate."
paul@1040 259
paul@1058 260
        finish_scheduling(self)
paul@1040 261
paul@1039 262
    def retract_scheduling(self):
paul@1039 263
paul@1039 264
        "Retract this event from scheduling records."
paul@1039 265
paul@1058 266
        retract_scheduling(self)
paul@1039 267
paul@131 268
class Event(ResourceHandler):
paul@48 269
paul@48 270
    "An event handler."
paul@48 271
paul@48 272
    def add(self):
paul@676 273
paul@676 274
        "Add a new occurrence to an existing event."
paul@676 275
paul@937 276
        self._process(self._add_for_attendee)
paul@48 277
paul@48 278
    def cancel(self):
paul@131 279
paul@131 280
        "Cancel attendance for attendees."
paul@131 281
paul@937 282
        self._process(self._cancel_for_attendee)
paul@48 283
paul@48 284
    def counter(self):
paul@48 285
paul@48 286
        "Since this handler does not send requests, it will not handle replies."
paul@48 287
paul@48 288
        pass
paul@48 289
paul@48 290
    def declinecounter(self):
paul@48 291
paul@710 292
        "Revoke any counter-proposal."
paul@48 293
paul@937 294
        self._process(self._revoke_for_attendee)
paul@48 295
paul@48 296
    def publish(self):
paul@676 297
paul@676 298
        """
paul@676 299
        Resources only consider events sent as requests, not generally published
paul@676 300
        events.
paul@676 301
        """
paul@676 302
paul@48 303
        pass
paul@48 304
paul@48 305
    def refresh(self):
paul@626 306
paul@626 307
        """
paul@626 308
        Refresh messages are typically sent to event organisers, but resources
paul@626 309
        do not act as organisers themselves.
paul@626 310
        """
paul@48 311
paul@676 312
        pass
paul@676 313
paul@48 314
    def reply(self):
paul@48 315
paul@48 316
        "Since this handler does not send requests, it will not handle replies."
paul@48 317
paul@48 318
        pass
paul@48 319
paul@48 320
    def request(self):
paul@48 321
paul@48 322
        """
paul@48 323
        Respond to a request by preparing a reply containing accept/decline
paul@468 324
        information for the recipient.
paul@48 325
paul@48 326
        No support for countering requests is implemented.
paul@48 327
        """
paul@48 328
paul@937 329
        self._process(self._schedule_for_attendee)
paul@48 330
paul@725 331
class Freebusy(CommonFreebusy, Handler):
paul@48 332
paul@48 333
    "A free/busy handler."
paul@48 334
paul@48 335
    def publish(self):
paul@676 336
paul@676 337
        "Resources ignore generally published free/busy information."
paul@676 338
paul@943 339
        self._record_freebusy(from_organiser=True)
paul@48 340
paul@48 341
    def reply(self):
paul@48 342
paul@48 343
        "Since this handler does not send requests, it will not handle replies."
paul@48 344
paul@48 345
        pass
paul@48 346
paul@108 347
    # request provided by CommonFreeBusy.request
paul@48 348
paul@48 349
# Handler registry.
paul@48 350
paul@48 351
handlers = [
paul@48 352
    ("VFREEBUSY",   Freebusy),
paul@48 353
    ("VEVENT",      Event),
paul@48 354
    ]
paul@48 355
paul@48 356
# vim: tabstop=4 expandtab shiftwidth=4