imip-agent

Annotated imiptools/handlers/person.py

129:39509f7669d4
2014-12-10 Paul Boddie Introduced address and URI normalisation for organisers and attendees. (This helps with the processing of Claws Mail vCalendar plugin messages.)
paul@55 1
#!/usr/bin/env python
paul@55 2
paul@55 3
"""
paul@55 4
Handlers for a person for whom scheduling is performed.
paul@55 5
"""
paul@55 6
paul@60 7
from email.mime.text import MIMEText
paul@70 8
from imiptools.config import MANAGER_PATH, MANAGER_URL
paul@129 9
from imiptools.content import Handler, get_address, get_uri, to_part, uri_dict, uri_items
paul@108 10
from imiptools.handlers.common import CommonFreebusy
paul@70 11
from socket import gethostname
paul@55 12
from vCalendar import to_node
paul@55 13
paul@70 14
def get_manager_url():
paul@70 15
    url_base = MANAGER_URL or "http://%s/" % gethostname()
paul@70 16
    return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/"))
paul@70 17
paul@67 18
class PersonHandler(Handler):
paul@55 19
paul@67 20
    "Handling mechanisms specific to people."
paul@55 21
paul@94 22
    def _record_and_deliver(self, objtype, from_organiser=True, queue=False):
paul@55 23
paul@100 24
        oa = self.require_organiser_and_attendees(from_organiser)
paul@55 25
        if not oa:
paul@61 26
            return False
paul@55 27
paul@94 28
        (organiser, organiser_attr), attendees = organiser_item, attendees = oa
paul@94 29
paul@94 30
        # Validate the organiser or attendee, ignoring spoofed requests.
paul@94 31
paul@100 32
        if not self.validate_identities(from_organiser and [organiser_item] or attendees.items()):
paul@94 33
            return False
paul@55 34
paul@110 35
        # Handle notifications and invitations.
paul@55 36
paul@100 37
        if from_organiser:
paul@110 38
paul@110 39
            # Process each attendee separately.
paul@110 40
paul@100 41
            for attendee, attendee_attr in attendees.items():
paul@100 42
paul@100 43
                if not self.have_new_object(attendee, objtype):
paul@100 44
                    continue
paul@100 45
paul@100 46
                # Store the object and queue any request.
paul@55 47
paul@100 48
                self.store.set_event(attendee, self.uid, to_node(
paul@100 49
                    {objtype : [(self.details, {})]}
paul@100 50
                    ))
paul@55 51
paul@100 52
                if queue:
paul@100 53
                    self.store.queue_request(attendee, self.uid)
paul@100 54
paul@100 55
        # As organiser, update attendance.
paul@55 56
paul@100 57
        else:
paul@100 58
            obj = self.get_object(organiser, objtype)
paul@100 59
paul@100 60
            if obj and self.have_new_object(organiser, objtype, obj):
paul@129 61
                attendee_map = uri_dict(self.get_value_map("ATTENDEE"))
paul@100 62
paul@100 63
                for attendee, attendee_attr in attendees.items():
paul@100 64
paul@100 65
                    # Update attendance in the loaded object.
paul@55 66
paul@100 67
                    attendee_map[attendee] = attendee_attr
paul@100 68
paul@100 69
                # Set the new details and store the object.
paul@100 70
paul@100 71
                obj["ATTENDEE"] = attendee_map.items()
paul@100 72
paul@100 73
                self.store.set_event(organiser, self.uid, to_node(
paul@100 74
                    {objtype : [(obj, {})]}
paul@100 75
                    ))
paul@61 76
paul@61 77
        return True
paul@61 78
paul@110 79
    def _record_freebusy(self, from_organiser=True):
paul@110 80
paul@110 81
        "Record free/busy information for the received information."
paul@110 82
paul@110 83
        freebusy = []
paul@111 84
paul@111 85
        for value in self.get_values("FREEBUSY") or []:
paul@110 86
            if not isinstance(value, list):
paul@110 87
                value = [value]
paul@110 88
            for v in value:
paul@110 89
                try:
paul@110 90
                    start, end = v.split("/", 1)
paul@110 91
                    freebusy.append((start, end))
paul@110 92
                except ValueError:
paul@110 93
                    pass
paul@110 94
paul@129 95
        for sender, sender_attr in uri_items(self.get_items(from_organiser and "ORGANIZER" or "ATTENDEE")):
paul@110 96
            for recipient in self.recipients:
paul@110 97
                self.store.set_freebusy_for_other(get_uri(recipient), freebusy, sender)
paul@110 98
paul@88 99
    def reply(self):
paul@88 100
paul@88 101
        "Wrap any valid message and pass it on to the recipient."
paul@88 102
paul@129 103
        attendee = get_address(self.get_value("ATTENDEE"))
paul@88 104
        if attendee:
paul@88 105
            return "REPLY", MIMEText("A reply has been received from %s." % attendee)
paul@88 106
paul@67 107
class Event(PersonHandler):
paul@67 108
paul@67 109
    "An event handler."
paul@67 110
paul@63 111
    def add(self):
paul@63 112
paul@63 113
        # NOTE: Queue a suggested modification to any active event.
paul@63 114
paul@63 115
        # The message is now wrapped and passed on to the recipient.
paul@63 116
paul@63 117
        return "ADD", MIMEText("An addition to an event has been received.")
paul@63 118
paul@63 119
    def cancel(self):
paul@63 120
paul@63 121
        # NOTE: Queue a suggested modification to any active event.
paul@63 122
paul@63 123
        # The message is now wrapped and passed on to the recipient.
paul@63 124
paul@63 125
        return "CANCEL", MIMEText("A cancellation has been received.")
paul@63 126
paul@63 127
    def counter(self):
paul@63 128
paul@63 129
        # NOTE: Queue a suggested modification to any active event.
paul@63 130
paul@63 131
        # The message is now wrapped and passed on to the recipient.
paul@63 132
paul@63 133
        return "COUNTER", MIMEText("A counter proposal has been received.")
paul@63 134
paul@63 135
    def declinecounter(self):
paul@63 136
paul@63 137
        # NOTE: Queue a suggested modification to any active event.
paul@63 138
paul@63 139
        # The message is now wrapped and passed on to the recipient.
paul@63 140
paul@63 141
        return "DECLINECOUNTER", MIMEText("A declining counter proposal has been received.")
paul@63 142
paul@63 143
    def publish(self):
paul@63 144
paul@63 145
        # NOTE: Register details of any relevant event.
paul@63 146
paul@63 147
        # The message is now wrapped and passed on to the recipient.
paul@63 148
paul@63 149
        return "PUBLISH", MIMEText("Details of an event have been received.")
paul@63 150
paul@63 151
    def refresh(self):
paul@63 152
paul@63 153
        # NOTE: Update details of any active event.
paul@63 154
paul@63 155
        # The message is now wrapped and passed on to the recipient.
paul@63 156
paul@63 157
        return "REFRESH", MIMEText("An event update has been received.")
paul@63 158
paul@61 159
    def reply(self):
paul@61 160
paul@61 161
        "Record replies and notify the recipient."
paul@61 162
paul@94 163
        self._record_and_deliver("VEVENT", from_organiser=False, queue=False)
paul@88 164
        return PersonHandler.reply(self)
paul@61 165
paul@61 166
    def request(self):
paul@61 167
paul@61 168
        "Hold requests and notify the recipient."
paul@61 169
paul@94 170
        self._record_and_deliver("VEVENT", from_organiser=True, queue=True)
paul@55 171
paul@55 172
        # The message is now wrapped and passed on to the recipient.
paul@55 173
paul@70 174
        url = "%s/%s" % (get_manager_url().rstrip("/"), self.uid)
paul@70 175
        return "REQUEST", MIMEText("A request has been queued and can be viewed here: %s" % url)
paul@60 176
paul@108 177
class Freebusy(PersonHandler, CommonFreebusy):
paul@55 178
paul@55 179
    "A free/busy handler."
paul@55 180
paul@55 181
    def publish(self):
paul@63 182
paul@110 183
        "Register free/busy information."
paul@110 184
paul@110 185
        self._record_freebusy(from_organiser=True)
paul@63 186
paul@63 187
        # The message is now wrapped and passed on to the recipient.
paul@63 188
paul@63 189
        return "PUBLISH", MIMEText("Details of a contact's availability have been received.")
paul@55 190
paul@55 191
    def reply(self):
paul@55 192
paul@63 193
        "Record replies and notify the recipient."
paul@63 194
paul@110 195
        self._record_freebusy(from_organiser=False)
paul@88 196
        return PersonHandler.reply(self)
paul@55 197
paul@55 198
    def request(self):
paul@55 199
paul@55 200
        """
paul@55 201
        Respond to a request by preparing a reply containing free/busy
paul@55 202
        information for each indicated attendee.
paul@55 203
        """
paul@55 204
paul@108 205
        # NOTE: This should be subject to policy/preferences.
paul@55 206
paul@108 207
        return CommonFreebusy.request(self)
paul@55 208
paul@67 209
class Journal(PersonHandler):
paul@55 210
paul@55 211
    "A journal entry handler."
paul@55 212
paul@55 213
    def add(self):
paul@63 214
paul@63 215
        # NOTE: Queue a suggested modification to any active entry.
paul@63 216
paul@63 217
        # The message is now wrapped and passed on to the recipient.
paul@63 218
paul@63 219
        return "ADD", MIMEText("An addition to a journal entry has been received.")
paul@55 220
paul@55 221
    def cancel(self):
paul@63 222
paul@63 223
        # NOTE: Queue a suggested modification to any active entry.
paul@63 224
paul@63 225
        # The message is now wrapped and passed on to the recipient.
paul@63 226
paul@63 227
        return "CANCEL", MIMEText("A cancellation has been received.")
paul@55 228
paul@55 229
    def publish(self):
paul@63 230
paul@63 231
        # NOTE: Register details of any relevant entry.
paul@63 232
paul@63 233
        # The message is now wrapped and passed on to the recipient.
paul@63 234
paul@63 235
        return "PUBLISH", MIMEText("Details of a journal entry have been received.")
paul@55 236
paul@67 237
class Todo(PersonHandler):
paul@55 238
paul@55 239
    "A to-do item handler."
paul@55 240
paul@55 241
    def add(self):
paul@63 242
paul@63 243
        # NOTE: Queue a suggested modification to any active item.
paul@63 244
paul@63 245
        # The message is now wrapped and passed on to the recipient.
paul@63 246
paul@63 247
        return "ADD", MIMEText("An addition to an item has been received.")
paul@55 248
paul@55 249
    def cancel(self):
paul@63 250
paul@63 251
        # NOTE: Queue a suggested modification to any active item.
paul@63 252
paul@63 253
        # The message is now wrapped and passed on to the recipient.
paul@63 254
paul@63 255
        return "CANCEL", MIMEText("A cancellation has been received.")
paul@55 256
paul@55 257
    def counter(self):
paul@55 258
paul@63 259
        # NOTE: Queue a suggested modification to any active item.
paul@55 260
paul@63 261
        # The message is now wrapped and passed on to the recipient.
paul@63 262
paul@63 263
        return "COUNTER", MIMEText("A counter proposal has been received.")
paul@55 264
paul@55 265
    def declinecounter(self):
paul@55 266
paul@63 267
        # NOTE: Queue a suggested modification to any active item.
paul@55 268
paul@63 269
        # The message is now wrapped and passed on to the recipient.
paul@63 270
paul@63 271
        return "DECLINECOUNTER", MIMEText("A declining counter proposal has been received.")
paul@55 272
paul@55 273
    def publish(self):
paul@63 274
paul@63 275
        # NOTE: Register details of any relevant item.
paul@63 276
paul@63 277
        # The message is now wrapped and passed on to the recipient.
paul@63 278
paul@63 279
        return "PUBLISH", MIMEText("Details of an item have been received.")
paul@55 280
paul@55 281
    def refresh(self):
paul@63 282
paul@63 283
        # NOTE: Update details of any active item.
paul@63 284
paul@63 285
        # The message is now wrapped and passed on to the recipient.
paul@63 286
paul@63 287
        return "REFRESH", MIMEText("An item update has been received.")
paul@55 288
paul@55 289
    def reply(self):
paul@55 290
paul@63 291
        "Record replies and notify the recipient."
paul@63 292
paul@94 293
        self._record_and_deliver("VTODO", from_organiser=False, queue=False)
paul@88 294
        return PersonHandler.reply(self)
paul@55 295
paul@55 296
    def request(self):
paul@63 297
paul@63 298
        "Hold requests and notify the recipient."
paul@63 299
paul@94 300
        self._record_and_deliver("VTODO", from_organiser=True, queue=True)
paul@63 301
paul@63 302
        # The message is now wrapped and passed on to the recipient.
paul@63 303
paul@63 304
        return "REQUEST", MIMEText("A request has been queued.")
paul@55 305
paul@55 306
# Handler registry.
paul@55 307
paul@55 308
handlers = [
paul@55 309
    ("VFREEBUSY",   Freebusy),
paul@55 310
    ("VEVENT",      Event),
paul@55 311
    ("VTODO",       Todo),
paul@55 312
    ("VJOURNAL",    Journal),
paul@55 313
    ]
paul@55 314
paul@55 315
# vim: tabstop=4 expandtab shiftwidth=4