imip-agent

Annotated imiptools/handlers/__init__.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@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@1176 26
                           get_sender_identities, uri_dict, uri_item
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@606 148
            return reduce(lambda a, b: a + b, [identities[get_uri(address)] for address in valid], [])
paul@418 149
        else:
paul@418 150
            return mapping
paul@418 151
paul@418 152
    def filter_by_recipient(self, mapping):
paul@418 153
paul@418 154
        """
paul@418 155
        Return a list of items from 'mapping' filtered using recipient
paul@418 156
        information.
paul@418 157
        """
paul@418 158
paul@418 159
        if self.recipient:
paul@418 160
            addresses = set(map(get_address, mapping))
paul@418 161
            return map(get_uri, addresses.intersection([self.recipient]))
paul@418 162
        else:
paul@418 163
            return mapping
paul@418 164
paul@1176 165
    def is_delegation(self):
paul@1176 166
paul@1176 167
        """
paul@1176 168
        Return whether delegation is occurring by returning any delegator.
paul@1176 169
        """
paul@1176 170
paul@1176 171
        attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
paul@1176 172
        attendee_attr = attendee_map.get(self.user)
paul@1176 173
        return check_delegation(attendee_map, self.user, attendee_attr)
paul@1176 174
paul@418 175
    def require_organiser(self, from_organiser=True):
paul@418 176
paul@418 177
        """
paul@1176 178
        Return the normalised organiser for the current object, filtered for the
paul@1176 179
        sender or recipient of interest. Return None if no identities are
paul@1176 180
        eligible.
paul@418 181
paul@1176 182
        If the sender is not the organiser but is delegating to the recipient,
paul@1176 183
        the actual organiser is returned.
paul@418 184
        """
paul@418 185
paul@712 186
        organiser, organiser_attr = organiser_item = uri_item(self.obj.get_item("ORGANIZER"))
paul@712 187
paul@712 188
        if not organiser:
paul@712 189
            return None
paul@418 190
paul@1176 191
        # Check the delegate status of the recipient.
paul@1176 192
paul@1176 193
        delegated = from_organiser and self.is_delegation()
paul@1176 194
paul@1176 195
        # Only provide details for an organiser who sent/receives the message or
paul@1176 196
        # is presiding over a delegation.
paul@418 197
paul@418 198
        organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient
paul@418 199
paul@1176 200
        if not delegated and not organiser_filter_fn(dict([organiser_item])):
paul@418 201
            return None
paul@418 202
paul@717 203
        # Test against any previously-received organiser details.
paul@717 204
paul@728 205
        if not self.is_recognised_organiser(organiser):
paul@734 206
            replacement = self.get_organiser_replacement()
paul@728 207
paul@728 208
            # Allow any organiser as a replacement where indicated.
paul@728 209
paul@728 210
            if replacement == "any":
paul@728 211
                pass
paul@728 212
paul@728 213
            # Allow any recognised attendee as a replacement where indicated.
paul@728 214
paul@728 215
            elif replacement != "attendee" or not self.is_recognised_attendee(organiser):
paul@717 216
                return None
paul@717 217
paul@418 218
        return organiser_item
paul@418 219
paul@418 220
    def require_attendees(self, from_organiser=True):
paul@418 221
paul@418 222
        """
paul@418 223
        Return the attendees for the current object, filtered for the sender or
paul@418 224
        recipient of interest. Return None if no identities are eligible.
paul@418 225
paul@418 226
        The attendee identities are normalized.
paul@418 227
        """
paul@418 228
paul@418 229
        attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
paul@418 230
paul@418 231
        # Only provide details for attendees who sent/receive the message.
paul@418 232
paul@418 233
        attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders
paul@418 234
paul@418 235
        attendees = {}
paul@418 236
        for attendee in attendee_filter_fn(attendee_map):
paul@712 237
            if attendee:
paul@712 238
                attendees[attendee] = attendee_map[attendee]
paul@418 239
paul@418 240
        return attendees
paul@418 241
paul@418 242
    def require_organiser_and_attendees(self, from_organiser=True):
paul@418 243
paul@418 244
        """
paul@418 245
        Return the organiser and attendees for the current object, filtered for
paul@418 246
        the recipient of interest. Return None if no identities are eligible.
paul@418 247
paul@418 248
        Organiser and attendee identities are normalized.
paul@418 249
        """
paul@418 250
paul@418 251
        organiser_item = self.require_organiser(from_organiser)
paul@418 252
        attendees = self.require_attendees(from_organiser)
paul@418 253
paul@418 254
        if not attendees or not organiser_item:
paul@418 255
            return None
paul@418 256
paul@418 257
        return organiser_item, attendees
paul@418 258
paul@418 259
# vim: tabstop=4 expandtab shiftwidth=4