imip-agent

Annotated imiptools/__init__.py

178:60f1a06d2290
2015-01-27 Paul Boddie Added support for different message and summary delivery preferences. Simplified the summary message structure, avoiding unnecessary nesting.
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@178 23
from imiptools.content import get_addresses, get_uri, handle_itip_part
paul@83 24
from imiptools.mail import Messenger
paul@178 25
from imiptools.profile import Preferences
paul@49 26
import sys
paul@49 27
paul@49 28
# Postfix exit codes.
paul@49 29
paul@49 30
EX_TEMPFAIL     = 75
paul@49 31
paul@49 32
# Permitted iTIP content types.
paul@49 33
paul@49 34
itip_content_types = [
paul@49 35
    "text/calendar",                        # from RFC 6047
paul@49 36
    "text/x-vcalendar", "application/ics",  # other possibilities
paul@49 37
    ]
paul@49 38
paul@49 39
# Processing of incoming messages.
paul@49 40
paul@49 41
def get_all_values(msg, key):
paul@49 42
    l = []
paul@49 43
    for v in msg.get_all(key) or []:
paul@49 44
        l += [s.strip() for s in v.split(",")]
paul@49 45
    return l
paul@49 46
paul@49 47
class Processor:
paul@49 48
paul@49 49
    "The processing framework."
paul@49 50
paul@82 51
    def __init__(self, handlers, messenger=None):
paul@49 52
        self.handlers = handlers
paul@82 53
        self.messenger = messenger or Messenger()
paul@60 54
        self.lmtp_socket = None
paul@49 55
paul@96 56
    def process(self, f, original_recipients, recipients, outgoing_only):
paul@49 57
paul@49 58
        """
paul@49 59
        Process content from the stream 'f' accompanied by the given
paul@49 60
        'original_recipients' and 'recipients'.
paul@49 61
        """
paul@49 62
paul@49 63
        msg = message_from_file(f)
paul@178 64
        senders = get_addresses(msg.get_all("Reply-To") or msg.get_all("From") or [])
paul@178 65
        original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])
paul@49 66
paul@49 67
        # Handle messages with iTIP parts.
paul@49 68
paul@60 69
        all_responses = []
paul@60 70
        handled = False
paul@49 71
paul@49 72
        for part in msg.walk():
paul@49 73
            if part.get_content_type() in itip_content_types and \
paul@49 74
                part.get_param("method"):
paul@49 75
paul@89 76
                all_responses += handle_itip_part(part, senders, original_recipients, self.handlers, self.messenger)
paul@60 77
                handled = True
paul@49 78
paul@96 79
        # When processing outgoing messages, no replies or deliveries are
paul@96 80
        # performed.
paul@96 81
paul@96 82
        if outgoing_only:
paul@96 83
            return
paul@96 84
paul@178 85
        # Pack any returned parts into messages.
paul@49 86
paul@60 87
        if all_responses:
paul@60 88
            outgoing_parts = []
paul@60 89
            forwarded_parts = []
paul@60 90
paul@60 91
            for outgoing, part in all_responses:
paul@60 92
                if outgoing:
paul@60 93
                    outgoing_parts.append(part)
paul@60 94
                else:
paul@60 95
                    forwarded_parts.append(part)
paul@60 96
paul@60 97
            # Reply using any outgoing parts in a new message.
paul@60 98
paul@60 99
            if outgoing_parts:
paul@178 100
                message = self.messenger.make_outgoing_message(outgoing_parts, senders)
paul@49 101
paul@60 102
                if "-d" in sys.argv:
paul@106 103
                    print >>sys.stderr, "Outgoing parts..."
paul@60 104
                    print message
paul@60 105
                else:
paul@82 106
                    self.messenger.sendmail(senders, message.as_string())
paul@60 107
paul@178 108
            # Forward messages to their recipients either wrapping the existing
paul@178 109
            # message, accompanying it or replacing it.
paul@60 110
paul@60 111
            if forwarded_parts:
paul@178 112
paul@178 113
                # Determine whether to wrap, accompany or replace the message.
paul@178 114
paul@178 115
                for recipient in original_recipients:
paul@178 116
                    preferences = Preferences(get_uri(recipient))
paul@178 117
paul@178 118
                    incoming = preferences.get("incoming")
paul@60 119
paul@178 120
                    if incoming == "message-only":
paul@178 121
                        messages = [msg]
paul@178 122
                    else:
paul@178 123
                        summary = self.messenger.make_summary_message(msg, forwarded_parts)
paul@178 124
                        if incoming == "summary-then-message":
paul@178 125
                            messages = [summary, msg]
paul@178 126
                        elif incoming == "message-then-summary":
paul@178 127
                            messages = [msg, summary]
paul@178 128
                        elif incoming == "summary-only":
paul@178 129
                            messages = [summary]
paul@178 130
                        else: # incoming == "summary-wraps-message":
paul@178 131
                            messages = [self.messenger.wrap_message(msg, forwarded_parts)]
paul@178 132
paul@178 133
                    for message in messages:
paul@178 134
                        if "-d" in sys.argv:
paul@178 135
                            print >>sys.stderr, "Forwarded parts..."
paul@178 136
                            print message
paul@178 137
                        elif self.lmtp_socket:
paul@178 138
                            self.messenger.sendmail(recipient, message.as_string(), lmtp_socket=self.lmtp_socket)
paul@60 139
paul@60 140
        # Unhandled messages are delivered as they are.
paul@60 141
paul@60 142
        if not handled:
paul@49 143
            if "-d" in sys.argv:
paul@106 144
                print >>sys.stderr, "Unhandled parts..."
paul@60 145
                print msg
paul@60 146
            elif self.lmtp_socket:
paul@86 147
                self.messenger.sendmail(original_recipients, msg.as_string(), lmtp_socket=self.lmtp_socket)
paul@64 148
paul@49 149
    def process_args(self, args, stream):
paul@49 150
paul@49 151
        """
paul@49 152
        Interpret the given program arguments 'args' and process input from the
paul@49 153
        given 'stream'.
paul@49 154
        """
paul@49 155
paul@49 156
        # Obtain the different kinds of recipients plus sender address.
paul@49 157
paul@49 158
        original_recipients = []
paul@49 159
        recipients = []
paul@49 160
        senders = []
paul@60 161
        lmtp = []
paul@96 162
        outgoing_only = False
paul@49 163
paul@49 164
        l = []
paul@49 165
paul@49 166
        for arg in args:
paul@49 167
paul@96 168
            # Detect outgoing processing mode.
paul@96 169
paul@96 170
            if arg == "-O":
paul@96 171
                outgoing_only = True
paul@96 172
paul@49 173
            # Switch to collecting recipients.
paul@49 174
paul@49 175
            if arg == "-o":
paul@49 176
                l = original_recipients
paul@49 177
            elif arg == "-r":
paul@49 178
                l = recipients
paul@49 179
paul@49 180
            # Switch to collecting senders.
paul@49 181
paul@49 182
            elif arg == "-s":
paul@49 183
                l = senders
paul@49 184
paul@60 185
            # Switch to getting the LMTP socket.
paul@60 186
paul@60 187
            elif arg == "-l":
paul@60 188
                l = lmtp
paul@60 189
paul@49 190
            # Ignore debugging options.
paul@49 191
paul@49 192
            elif arg == "-d":
paul@49 193
                pass
paul@49 194
            else:
paul@49 195
                l.append(arg)
paul@49 196
paul@82 197
        self.messenger.sender = senders and senders[0] or self.messenger.sender
paul@60 198
        self.lmtp_socket = lmtp and lmtp[0] or None
paul@96 199
        self.process(stream, original_recipients, recipients, outgoing_only)
paul@49 200
paul@49 201
    def __call__(self):
paul@49 202
paul@49 203
        """
paul@49 204
        Obtain arguments from the command line to initialise the processor
paul@49 205
        before invoking it.
paul@49 206
        """
paul@49 207
paul@49 208
        args = sys.argv[1:]
paul@49 209
paul@49 210
        if "-d" in args:
paul@49 211
            self.process_args(args, sys.stdin)
paul@49 212
        else:
paul@49 213
            try:
paul@49 214
                self.process_args(args, sys.stdin)
paul@49 215
            except SystemExit, value:
paul@49 216
                sys.exit(value)
paul@49 217
            except Exception, exc:
paul@60 218
                if "-v" in args:
paul@60 219
                    raise
paul@49 220
                type, value, tb = sys.exc_info()
paul@49 221
                print >>sys.stderr, "Exception %s at %d" % (exc, tb.tb_lineno)
paul@82 222
                #import traceback
paul@82 223
                #traceback.print_exc(file=open("/tmp/mail.log", "a"))
paul@49 224
                sys.exit(EX_TEMPFAIL)
paul@49 225
        sys.exit(0)
paul@49 226
paul@49 227
# vim: tabstop=4 expandtab shiftwidth=4