imip-agent

Annotated imiptools/__init__.py

833:8ebca6ce3973
2015-10-14 Paul Boddie Fixed printable representations.
paul@49 1
#!/usr/bin/env python
paul@49 2
paul@146 3
"""
paul@146 4
A processing framework for iMIP content.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 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@146 20
"""
paul@146 21
paul@49 22
from email import message_from_file
paul@749 23
from imiptools import config
paul@604 24
from imiptools.client import Client
paul@213 25
from imiptools.content import handle_itip_part
paul@604 26
from imiptools.data import get_address, get_addresses, get_uri
paul@83 27
from imiptools.mail import Messenger
paul@223 28
import imip_store
paul@49 29
import sys
paul@49 30
paul@49 31
# Postfix exit codes.
paul@49 32
paul@49 33
EX_TEMPFAIL     = 75
paul@49 34
paul@49 35
# Permitted iTIP content types.
paul@49 36
paul@49 37
itip_content_types = [
paul@49 38
    "text/calendar",                        # from RFC 6047
paul@49 39
    "text/x-vcalendar", "application/ics",  # other possibilities
paul@49 40
    ]
paul@49 41
paul@49 42
# Processing of incoming messages.
paul@49 43
paul@49 44
def get_all_values(msg, key):
paul@49 45
    l = []
paul@49 46
    for v in msg.get_all(key) or []:
paul@49 47
        l += [s.strip() for s in v.split(",")]
paul@49 48
    return l
paul@49 49
paul@49 50
class Processor:
paul@49 51
paul@49 52
    "The processing framework."
paul@49 53
paul@822 54
    def __init__(self, handlers, outgoing_only=False):
paul@49 55
        self.handlers = handlers
paul@822 56
        self.outgoing_only = outgoing_only
paul@607 57
        self.messenger = None
paul@60 58
        self.lmtp_socket = None
paul@563 59
        self.store_dir = None
paul@563 60
        self.publishing_dir = None
paul@639 61
        self.preferences_dir = None
paul@563 62
        self.debug = False
paul@49 63
paul@569 64
    def get_store(self):
paul@598 65
        return imip_store.FileStore(self.store_dir)
paul@569 66
paul@569 67
    def get_publisher(self):
paul@569 68
        return self.publishing_dir and imip_store.FilePublisher(self.publishing_dir) or None
paul@569 69
paul@822 70
    def process(self, f, original_recipients):
paul@49 71
paul@49 72
        """
paul@49 73
        Process content from the stream 'f' accompanied by the given
paul@515 74
        'original_recipients'.
paul@49 75
        """
paul@49 76
paul@49 77
        msg = message_from_file(f)
paul@677 78
        senders = get_addresses(get_all_values(msg, "Reply-To") or get_all_values(msg, "From") or [])
paul@49 79
paul@604 80
        messenger = self.messenger
paul@604 81
        store = self.get_store()
paul@604 82
        publisher = self.get_publisher()
paul@639 83
        preferences_dir = self.preferences_dir
paul@604 84
paul@49 85
        # Handle messages with iTIP parts.
paul@438 86
        # Typically, the details of recipients are of interest in handling
paul@438 87
        # messages.
paul@49 88
paul@822 89
        if not self.outgoing_only:
paul@438 90
            original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])
paul@438 91
            for recipient in original_recipients:
paul@822 92
                Recipient(get_uri(recipient), messenger, store, publisher, preferences_dir, self.handlers, self.outgoing_only, self.debug
paul@822 93
                         ).process(msg, senders)
paul@438 94
paul@438 95
        # However, outgoing messages do not usually presume anything about the
paul@832 96
        # eventual recipients and focus on the sender instead. If possible, the
paul@832 97
        # sender is identified, but since this may be the calendar system (and
paul@832 98
        # the actual sender is defined in the object), and since the recipient
paul@832 99
        # may be in a Bcc header that is not available here, it may be left as
paul@832 100
        # None and deduced from the object content later. 
paul@438 101
paul@438 102
        else:
paul@832 103
            senders = [sender for sender in get_addresses(get_all_values(msg, "From") or []) if sender != config.MESSAGE_SENDER]
paul@832 104
            Recipient(senders and senders[0] or None, messenger, store, publisher, preferences_dir, self.handlers, self.outgoing_only, self.debug
paul@822 105
                     ).process(msg, senders)
paul@229 106
paul@49 107
    def process_args(self, args, stream):
paul@49 108
paul@49 109
        """
paul@49 110
        Interpret the given program arguments 'args' and process input from the
paul@49 111
        given 'stream'.
paul@49 112
        """
paul@49 113
paul@49 114
        # Obtain the different kinds of recipients plus sender address.
paul@49 115
paul@49 116
        original_recipients = []
paul@49 117
        recipients = []
paul@49 118
        senders = []
paul@60 119
        lmtp = []
paul@563 120
        store_dir = []
paul@563 121
        publishing_dir = []
paul@639 122
        preferences_dir = []
paul@666 123
        local_smtp = False
paul@49 124
paul@49 125
        l = []
paul@49 126
paul@49 127
        for arg in args:
paul@49 128
paul@49 129
            # Switch to collecting recipients.
paul@49 130
paul@822 131
            if arg == "-o":
paul@49 132
                l = original_recipients
paul@49 133
paul@49 134
            # Switch to collecting senders.
paul@49 135
paul@49 136
            elif arg == "-s":
paul@49 137
                l = senders
paul@49 138
paul@60 139
            # Switch to getting the LMTP socket.
paul@60 140
paul@60 141
            elif arg == "-l":
paul@60 142
                l = lmtp
paul@60 143
paul@666 144
            # Detect sending to local users via SMTP.
paul@666 145
paul@666 146
            elif arg == "-L":
paul@666 147
                local_smtp = True
paul@666 148
paul@563 149
            # Switch to getting the store directory.
paul@563 150
paul@563 151
            elif arg == "-S":
paul@563 152
                l = store_dir
paul@563 153
paul@563 154
            # Switch to getting the publishing directory.
paul@563 155
paul@563 156
            elif arg == "-P":
paul@563 157
                l = publishing_dir
paul@563 158
paul@639 159
            # Switch to getting the preferences directory.
paul@639 160
paul@639 161
            elif arg == "-p":
paul@639 162
                l = preferences_dir
paul@639 163
paul@49 164
            # Ignore debugging options.
paul@49 165
paul@49 166
            elif arg == "-d":
paul@563 167
                self.debug = True
paul@49 168
            else:
paul@49 169
                l.append(arg)
paul@49 170
paul@666 171
        self.messenger = Messenger(lmtp_socket=lmtp and lmtp[0] or None, local_smtp=local_smtp, sender=senders and senders[0] or None)
paul@563 172
        self.store_dir = store_dir and store_dir[0] or None
paul@563 173
        self.publishing_dir = publishing_dir and publishing_dir[0] or None
paul@639 174
        self.preferences_dir = preferences_dir and preferences_dir[0] or None
paul@822 175
        self.process(stream, original_recipients)
paul@49 176
paul@49 177
    def __call__(self):
paul@49 178
paul@49 179
        """
paul@49 180
        Obtain arguments from the command line to initialise the processor
paul@49 181
        before invoking it.
paul@49 182
        """
paul@49 183
paul@49 184
        args = sys.argv[1:]
paul@49 185
paul@49 186
        if "-d" in args:
paul@49 187
            self.process_args(args, sys.stdin)
paul@49 188
        else:
paul@49 189
            try:
paul@49 190
                self.process_args(args, sys.stdin)
paul@49 191
            except SystemExit, value:
paul@49 192
                sys.exit(value)
paul@49 193
            except Exception, exc:
paul@60 194
                if "-v" in args:
paul@60 195
                    raise
paul@49 196
                type, value, tb = sys.exc_info()
paul@310 197
                while tb.tb_next:
paul@310 198
                    tb = tb.tb_next
paul@310 199
                f = tb.tb_frame
paul@310 200
                co = f and f.f_code
paul@310 201
                filename = co and co.co_filename
paul@310 202
                print >>sys.stderr, "Exception %s at %d in %s" % (exc, tb.tb_lineno, filename)
paul@82 203
                #import traceback
paul@82 204
                #traceback.print_exc(file=open("/tmp/mail.log", "a"))
paul@49 205
                sys.exit(EX_TEMPFAIL)
paul@49 206
        sys.exit(0)
paul@49 207
paul@604 208
class Recipient(Client):
paul@604 209
paul@604 210
    "A processor acting as a client on behalf of a recipient."
paul@604 211
paul@822 212
    def __init__(self, user, messenger, store, publisher, preferences_dir, handlers, outgoing_only, debug):
paul@604 213
paul@604 214
        """
paul@604 215
        Initialise the recipient with the given 'user' identity, 'messenger',
paul@822 216
        'store', 'publisher', 'preferences_dir', 'handlers', 'outgoing_only' and
paul@822 217
        'debug' status.
paul@604 218
        """
paul@604 219
paul@639 220
        Client.__init__(self, user, messenger, store, publisher, preferences_dir)
paul@607 221
        self.handlers = handlers
paul@822 222
        self.outgoing_only = outgoing_only
paul@607 223
        self.debug = debug
paul@604 224
paul@822 225
    def process(self, msg, senders):
paul@604 226
paul@604 227
        """
paul@604 228
        Process the given 'msg' for a single recipient, having the given
paul@822 229
        'senders'.
paul@604 230
paul@604 231
        Processing individually means that contributions to resulting messages
paul@604 232
        may be constructed according to individual preferences.
paul@604 233
        """
paul@604 234
paul@604 235
        handlers = dict([(name, cls(senders, self.user and get_address(self.user),
paul@639 236
                                    self.messenger, self.store, self.publisher,
paul@639 237
                                    self.preferences_dir))
paul@607 238
                         for name, cls in self.handlers])
paul@604 239
        handled = False
paul@604 240
paul@667 241
        # Check for participating recipients. Non-participating recipients will
paul@667 242
        # have their messages left as being unhandled.
paul@667 243
paul@822 244
        if self.outgoing_only or self.is_participating():
paul@667 245
paul@667 246
            # Check for returned messages.
paul@665 247
paul@665 248
            for part in msg.walk():
paul@667 249
                if part.get_content_type() == "message/delivery-status":
paul@667 250
                    break
paul@667 251
            else:
paul@667 252
                for part in msg.walk():
paul@667 253
                    if part.get_content_type() in itip_content_types and \
paul@667 254
                       part.get_param("method"):
paul@604 255
paul@667 256
                        handle_itip_part(part, handlers)
paul@667 257
                        handled = True
paul@604 258
paul@604 259
        # When processing outgoing messages, no replies or deliveries are
paul@604 260
        # performed.
paul@604 261
paul@822 262
        if self.outgoing_only:
paul@604 263
            return
paul@604 264
paul@604 265
        # Get responses from the handlers.
paul@604 266
paul@604 267
        all_responses = []
paul@604 268
        for handler in handlers.values():
paul@604 269
            all_responses += handler.get_results()
paul@604 270
paul@604 271
        # Pack any returned parts into messages.
paul@604 272
paul@604 273
        if all_responses:
paul@604 274
            outgoing_parts = {}
paul@604 275
            forwarded_parts = []
paul@604 276
paul@604 277
            for outgoing_recipients, part in all_responses:
paul@604 278
                if outgoing_recipients:
paul@604 279
                    for outgoing_recipient in outgoing_recipients:
paul@604 280
                        if not outgoing_parts.has_key(outgoing_recipient):
paul@604 281
                            outgoing_parts[outgoing_recipient] = []
paul@604 282
                        outgoing_parts[outgoing_recipient].append(part)
paul@604 283
                else:
paul@604 284
                    forwarded_parts.append(part)
paul@604 285
paul@604 286
            # Reply using any outgoing parts in a new message.
paul@604 287
paul@604 288
            if outgoing_parts:
paul@604 289
paul@604 290
                # Obtain free/busy details, if configured to do so.
paul@604 291
paul@604 292
                fb = self.can_provide_freebusy(handlers) and self.get_freebusy_part()
paul@604 293
paul@604 294
                for outgoing_recipient, parts in outgoing_parts.items():
paul@604 295
paul@604 296
                    # Bundle free/busy messages, if configured to do so.
paul@604 297
paul@604 298
                    if fb: parts.append(fb)
paul@604 299
                    message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])
paul@604 300
paul@607 301
                    if self.debug:
paul@604 302
                        print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient
paul@604 303
                        print message
paul@604 304
                    else:
paul@604 305
                        self.messenger.sendmail([outgoing_recipient], message.as_string())
paul@604 306
paul@604 307
            # Forward messages to their recipients either wrapping the existing
paul@604 308
            # message, accompanying it or replacing it.
paul@604 309
paul@604 310
            if forwarded_parts:
paul@604 311
paul@604 312
                # Determine whether to wrap, accompany or replace the message.
paul@604 313
paul@604 314
                prefs = self.get_preferences()
paul@749 315
                incoming = prefs.get("incoming", config.INCOMING_DEFAULT)
paul@604 316
paul@604 317
                if incoming == "message-only":
paul@604 318
                    messages = [msg]
paul@604 319
                else:
paul@604 320
                    summary = self.messenger.make_summary_message(msg, forwarded_parts)
paul@604 321
                    if incoming == "summary-then-message":
paul@604 322
                        messages = [summary, msg]
paul@604 323
                    elif incoming == "message-then-summary":
paul@604 324
                        messages = [msg, summary]
paul@604 325
                    elif incoming == "summary-only":
paul@604 326
                        messages = [summary]
paul@604 327
                    else: # incoming == "summary-wraps-message":
paul@604 328
                        messages = [self.messenger.wrap_message(msg, forwarded_parts)]
paul@604 329
paul@604 330
                for message in messages:
paul@607 331
                    if self.debug:
paul@604 332
                        print >>sys.stderr, "Forwarded parts..."
paul@604 333
                        print message
paul@607 334
                    elif self.messenger.local_delivery():
paul@666 335
                        self.messenger.sendmail([get_address(self.user)], message.as_string())
paul@604 336
paul@604 337
        # Unhandled messages are delivered as they are.
paul@604 338
paul@604 339
        if not handled:
paul@607 340
            if self.debug:
paul@604 341
                print >>sys.stderr, "Unhandled parts..."
paul@604 342
                print msg
paul@607 343
            elif self.messenger.local_delivery():
paul@666 344
                self.messenger.sendmail([get_address(self.user)], msg.as_string())
paul@604 345
paul@604 346
    def can_provide_freebusy(self, handlers):
paul@604 347
paul@604 348
        "Test for any free/busy information produced by 'handlers'."
paul@604 349
paul@604 350
        fbhandler = handlers.get("VFREEBUSY")
paul@604 351
        if fbhandler:
paul@604 352
            fbmethods = fbhandler.get_outgoing_methods()
paul@604 353
            return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods
paul@604 354
        else:
paul@604 355
            return False
paul@604 356
paul@49 357
# vim: tabstop=4 expandtab shiftwidth=4