imip-agent

Annotated imiptools/handlers/__init__.py

1358:365abd0d41b4
2017-10-20 Paul Boddie Merged changes from the default branch. client-editing-simplification
paul@418 1
#!/usr/bin/env python
paul@418 2
paul@418 3
"""
paul@418 4
General handler support for incoming calendar objects.
paul@418 5
paul@1210 6
Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>
paul@418 7
paul@418 8
This program is free software; you can redistribute it and/or modify it under
paul@418 9
the terms of the GNU General Public License as published by the Free Software
paul@418 10
Foundation; either version 3 of the License, or (at your option) any later
paul@418 11
version.
paul@418 12
paul@418 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@418 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@418 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@418 16
details.
paul@418 17
paul@418 18
You should have received a copy of the GNU General Public License along with
paul@418 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@418 20
"""
paul@418 21
paul@418 22
from email.mime.text import MIMEText
paul@601 23
from imiptools.client import ClientForObject
paul@1210 24
from imiptools.config import settings
paul@1176 25
from imiptools.data import check_delegation, get_address, get_uri, \
paul@1336 26
                           get_sender_identities
paul@418 27
from socket import gethostname
paul@418 28
paul@1210 29
MANAGER_PATH = settings["MANAGER_PATH"]
paul@1210 30
MANAGER_URL = settings["MANAGER_URL"]
paul@1210 31
MANAGER_URL_SCHEME = settings["MANAGER_URL_SCHEME"]
paul@1210 32
paul@418 33
# References to the Web interface.
paul@418 34
paul@418 35
def get_manager_url():
paul@986 36
    url_base = MANAGER_URL or \
paul@986 37
               "%s%s/" % (MANAGER_URL_SCHEME or "https://", gethostname())
paul@418 38
    return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/"))
paul@418 39
paul@418 40
def get_object_url(uid, recurrenceid=None):
paul@418 41
    return "%s/%s%s" % (
paul@418 42
        get_manager_url().rstrip("/"), uid,
paul@418 43
        recurrenceid and "/%s" % recurrenceid or ""
paul@418 44
        )
paul@418 45
paul@601 46
class Handler(ClientForObject):
paul@418 47
paul@418 48
    "General handler support."
paul@418 49
paul@563 50
    def __init__(self, senders=None, recipient=None, messenger=None, store=None,
paul@1039 51
                 publisher=None, journal=None, preferences_dir=None):
paul@418 52
paul@418 53
        """
paul@601 54
        Initialise the handler with any specifically indicated 'senders' and
paul@601 55
        'recipient' of a calendar object. The object is initially undefined.
paul@601 56
paul@601 57
        The optional 'messenger' provides a means of interacting with the mail
paul@601 58
        system.
paul@563 59
paul@1039 60
        The optional 'store', 'publisher' and 'journal' can be specified to
paul@1039 61
        override the default store and publisher objects.
paul@418 62
        """
paul@418 63
paul@1039 64
        ClientForObject.__init__(self, None, recipient and get_uri(recipient),
paul@1039 65
            messenger, store, publisher, journal, preferences_dir)
paul@468 66
paul@418 67
        self.senders = senders and set(map(get_address, senders))
paul@418 68
        self.recipient = recipient and get_address(recipient)
paul@418 69
paul@418 70
        self.results = []
paul@418 71
        self.outgoing_methods = set()
paul@418 72
paul@418 73
    def wrap(self, text, link=True):
paul@418 74
paul@418 75
        "Wrap any valid message for passing to the recipient."
paul@418 76
paul@1005 77
        _ = self.get_translator()
paul@1005 78
paul@418 79
        texts = []
paul@418 80
        texts.append(text)
paul@1200 81
paul@1200 82
        # Add a link to the manager application if available and requested.
paul@1200 83
paul@668 84
        if link and self.have_manager():
paul@1005 85
            texts.append(_("If your mail program cannot handle this "
paul@1200 86
                           "message, you may view the details here:\n\n%s\n") %
paul@418 87
                         get_object_url(self.uid, self.recurrenceid))
paul@418 88
paul@1200 89
        # Create the text part, tagging it with a header that allows this part
paul@1200 90
        # to be merged with other calendar information.
paul@1200 91
paul@1200 92
        text_part = MIMEText("\n\n".join(texts))
paul@1200 93
        text_part["X-IMIP-Agent"] = "info"
paul@1200 94
        return self.add_result(None, None, text_part)
paul@418 95
paul@418 96
    # Result registration.
paul@418 97
paul@418 98
    def add_result(self, method, outgoing_recipients, part):
paul@418 99
paul@418 100
        """
paul@418 101
        Record a result having the given 'method', 'outgoing_recipients' and
paul@864 102
        message 'part'.
paul@418 103
        """
paul@418 104
paul@418 105
        if outgoing_recipients:
paul@418 106
            self.outgoing_methods.add(method)
paul@418 107
        self.results.append((outgoing_recipients, part))
paul@418 108
paul@864 109
    def add_results(self, methods, outgoing_recipients, parts):
paul@864 110
paul@864 111
        """
paul@864 112
        Record results having the given 'methods', 'outgoing_recipients' and
paul@864 113
        message 'parts'.
paul@864 114
        """
paul@864 115
paul@864 116
        if outgoing_recipients:
paul@864 117
            self.outgoing_methods.update(methods)
paul@864 118
        for part in parts:
paul@864 119
            self.results.append((outgoing_recipients, part))
paul@864 120
paul@418 121
    def get_results(self):
paul@418 122
        return self.results
paul@418 123
paul@418 124
    def get_outgoing_methods(self):
paul@418 125
        return self.outgoing_methods
paul@418 126
paul@418 127
    # Logic, filtering and access to calendar structures and other data.
paul@418 128
paul@418 129
    def filter_by_senders(self, mapping):
paul@418 130
paul@418 131
        """
paul@418 132
        Return a list of items from 'mapping' filtered using sender information.
paul@418 133
        """
paul@418 134
paul@418 135
        if self.senders:
paul@418 136
paul@418 137
            # Get a mapping from senders to identities.
paul@418 138
paul@606 139
            identities = get_sender_identities(mapping)
paul@418 140
paul@418 141
            # Find the senders that are valid.
paul@418 142
paul@418 143
            senders = map(get_address, identities)
paul@418 144
            valid = self.senders.intersection(senders)
paul@418 145
paul@418 146
            # Return the true identities.
paul@418 147
paul@1325 148
            attendees = []
paul@1325 149
            for address in valid:
paul@1325 150
                attendees += identities[get_uri(address)]
paul@1325 151
            return attendees
paul@1325 152
paul@1325 153
        # Rely on the mapping keys being accessible as a sequence.
paul@1325 154
paul@418 155
        else:
paul@418 156
            return mapping
paul@418 157
paul@418 158
    def filter_by_recipient(self, mapping):
paul@418 159
paul@418 160
        """
paul@418 161
        Return a list of items from 'mapping' filtered using recipient
paul@418 162
        information.
paul@418 163
        """
paul@418 164
paul@418 165
        if self.recipient:
paul@418 166
            addresses = set(map(get_address, mapping))
paul@418 167
            return map(get_uri, addresses.intersection([self.recipient]))
paul@1325 168
paul@1325 169
        # Rely on the mapping keys being accessible as a sequence.
paul@1325 170
paul@418 171
        else:
paul@418 172
            return mapping
paul@418 173
paul@1176 174
    def is_delegation(self):
paul@1176 175
paul@1176 176
        """
paul@1176 177
        Return whether delegation is occurring by returning any delegator.
paul@1176 178
        """
paul@1176 179
paul@1336 180
        attendee_map = self.obj.get_uri_map("ATTENDEE")
paul@1176 181
        attendee_attr = attendee_map.get(self.user)
paul@1176 182
        return check_delegation(attendee_map, self.user, attendee_attr)
paul@1176 183
paul@418 184
    def require_organiser(self, from_organiser=True):
paul@418 185
paul@418 186
        """
paul@1176 187
        Return the normalised organiser for the current object, filtered for the
paul@1176 188
        sender or recipient of interest. Return None if no identities are
paul@1176 189
        eligible.
paul@418 190
paul@1176 191
        If the sender is not the organiser but is delegating to the recipient,
paul@1176 192
        the actual organiser is returned.
paul@418 193
        """
paul@418 194
paul@1336 195
        organiser, organiser_attr = organiser_item = self.obj.get_uri_item("ORGANIZER")
paul@712 196
paul@712 197
        if not organiser:
paul@712 198
            return None
paul@418 199
paul@1176 200
        # Check the delegate status of the recipient.
paul@1176 201
paul@1176 202
        delegated = from_organiser and self.is_delegation()
paul@1176 203
paul@1176 204
        # Only provide details for an organiser who sent/receives the message or
paul@1176 205
        # is presiding over a delegation.
paul@418 206
paul@418 207
        organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient
paul@418 208
paul@1176 209
        if not delegated and not organiser_filter_fn(dict([organiser_item])):
paul@418 210
            return None
paul@418 211
paul@717 212
        # Test against any previously-received organiser details.
paul@717 213
paul@728 214
        if not self.is_recognised_organiser(organiser):
paul@734 215
            replacement = self.get_organiser_replacement()
paul@728 216
paul@728 217
            # Allow any organiser as a replacement where indicated.
paul@728 218
paul@728 219
            if replacement == "any":
paul@728 220
                pass
paul@728 221
paul@728 222
            # Allow any recognised attendee as a replacement where indicated.
paul@728 223
paul@728 224
            elif replacement != "attendee" or not self.is_recognised_attendee(organiser):
paul@717 225
                return None
paul@717 226
paul@418 227
        return organiser_item
paul@418 228
paul@418 229
    def require_attendees(self, from_organiser=True):
paul@418 230
paul@418 231
        """
paul@418 232
        Return the attendees for the current object, filtered for the sender or
paul@418 233
        recipient of interest. Return None if no identities are eligible.
paul@418 234
paul@418 235
        The attendee identities are normalized.
paul@418 236
        """
paul@418 237
paul@1336 238
        attendee_map = self.obj.get_uri_map("ATTENDEE")
paul@418 239
paul@418 240
        # Only provide details for attendees who sent/receive the message.
paul@418 241
paul@418 242
        attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders
paul@418 243
paul@418 244
        attendees = {}
paul@418 245
        for attendee in attendee_filter_fn(attendee_map):
paul@712 246
            if attendee:
paul@712 247
                attendees[attendee] = attendee_map[attendee]
paul@418 248
paul@418 249
        return attendees
paul@418 250
paul@418 251
    def require_organiser_and_attendees(self, from_organiser=True):
paul@418 252
paul@418 253
        """
paul@418 254
        Return the organiser and attendees for the current object, filtered for
paul@418 255
        the recipient of interest. Return None if no identities are eligible.
paul@418 256
paul@418 257
        Organiser and attendee identities are normalized.
paul@418 258
        """
paul@418 259
paul@418 260
        organiser_item = self.require_organiser(from_organiser)
paul@418 261
        attendees = self.require_attendees(from_organiser)
paul@418 262
paul@418 263
        if not attendees or not organiser_item:
paul@418 264
            return None
paul@418 265
paul@418 266
        return organiser_item, attendees
paul@418 267
paul@418 268
# vim: tabstop=4 expandtab shiftwidth=4