imip-agent

Annotated imiptools/__init__.py

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