File: __init__.py

package info (click to toggle)
imip-agent 0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 2,056 kB
  • sloc: python: 9,888; sh: 4,480; sql: 144; makefile: 8
file content (426 lines) | stat: -rw-r--r-- 14,601 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
#!/usr/bin/env python

"""
A processing framework for iMIP content.

Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 3 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
details.

You should have received a copy of the GNU General Public License along with
this program.  If not, see <http://www.gnu.org/licenses/>.
"""

from email import message_from_file
from imiptools.config import settings
from imiptools.client import Client
from imiptools.content import handle_itip_part
from imiptools.data import get_address, get_addresses, get_uri
from imiptools.mail import Messenger
from imiptools.stores import get_store, get_publisher, get_journal
import sys, os

# Postfix exit codes.

EX_TEMPFAIL     = 75

# Permitted iTIP content types.

itip_content_types = [
    "text/calendar",                        # from RFC 6047
    "text/x-vcalendar", "application/ics",  # other possibilities
    ]

# Processing of incoming messages.

def get_all_values(msg, key):
    l = []
    for v in msg.get_all(key) or []:
        l += [s.strip() for s in v.split(",")]
    return l

class Processor:

    "The processing framework."

    def __init__(self, handlers, outgoing_only=False):
        self.handlers = handlers
        self.outgoing_only = outgoing_only
        self.messenger = None
        self.lmtp_socket = None
        self.store_type = None
        self.store_dir = None
        self.publishing_dir = None
        self.journal_dir = None
        self.preferences_dir = None
        self.debug = False

    def get_store(self):
        return get_store(self.store_type, self.store_dir)

    def get_publisher(self):
        return self.publishing_dir and get_publisher(self.publishing_dir) or None

    def get_journal(self):
        return get_journal(self.store_type, self.journal_dir)

    def process(self, f, original_recipients):

        """
        Process content from the stream 'f' accompanied by the given
        'original_recipients'.
        """

        msg = message_from_file(f)
        senders = get_addresses(get_all_values(msg, "Reply-To") or get_all_values(msg, "From") or [])

        messenger = self.messenger
        store = self.get_store()
        publisher = self.get_publisher()
        journal = self.get_journal()
        preferences_dir = self.preferences_dir

        # Handle messages with iTIP parts.
        # Typically, the details of recipients are of interest in handling
        # messages.

        if not self.outgoing_only:
            original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])
            for recipient in original_recipients:
                Recipient(get_uri(recipient), messenger, store, publisher, journal, preferences_dir, self.handlers, self.outgoing_only, self.debug
                         ).process(msg, senders)

        # However, outgoing messages do not usually presume anything about the
        # eventual recipients and focus on the sender instead. If possible, the
        # sender is identified, but since this may be the calendar system (and
        # the actual sender is defined in the object), and since the recipient
        # may be in a Bcc header that is not available here, it may be left as
        # None and deduced from the object content later. 

        else:
            senders = [sender for sender in get_addresses(get_all_values(msg, "From") or []) if sender != settings["MESSAGE_SENDER"]]
            Recipient(senders and senders[0] or None, messenger, store, publisher, journal, preferences_dir, self.handlers, self.outgoing_only, self.debug
                     ).process(msg, senders)

    def process_args(self, args, stream):

        """
        Interpret the given program arguments 'args' and process input from the
        given 'stream'.
        """

        # Obtain the different kinds of recipients plus sender address.

        original_recipients = []
        recipients = []
        senders = []
        lmtp = []
        store_type = []
        store_dir = []
        publishing_dir = []
        preferences_dir = []
        journal_dir = []
        local_smtp = False

        l = []

        for arg in args:

            # Switch to collecting recipients.

            if arg == "-o":
                l = original_recipients

            # Switch to collecting senders.

            elif arg == "-s":
                l = senders

            # Switch to getting the LMTP socket.

            elif arg == "-l":
                l = lmtp

            # Detect sending to local users via SMTP.

            elif arg == "-L":
                local_smtp = True

            # Switch to getting the store type.

            elif arg == "-T":
                l = store_type

            # Switch to getting the store directory.

            elif arg == "-S":
                l = store_dir

            # Switch to getting the publishing directory.

            elif arg == "-P":
                l = publishing_dir

            # Switch to getting the preferences directory.

            elif arg == "-p":
                l = preferences_dir

            # Switch to getting the journal directory.

            elif arg == "-j":
                l = journal_dir

            # Ignore debugging options.

            elif arg == "-d":
                self.debug = True
            else:
                l.append(arg)

        getvalue = lambda value, default=None: value and value[0] or default

        self.messenger = Messenger(lmtp_socket=getvalue(lmtp), local_smtp=local_smtp, sender=getvalue(senders))
        self.store_type = getvalue(store_type, settings["STORE_TYPE"])
        self.store_dir = getvalue(store_dir)
        self.publishing_dir = getvalue(publishing_dir)
        self.preferences_dir = getvalue(preferences_dir)
        self.journal_dir = getvalue(journal_dir)

        # If debug mode is set, extend the line length for convenience.

        if self.debug:
            settings["CALENDAR_LINE_LENGTH"] = 1000

        # Process the input.

        self.process(stream, original_recipients)

    def __call__(self):

        """
        Obtain arguments from the command line to initialise the processor
        before invoking it.
        """

        args = sys.argv[1:]

        if "--help" in args:
            print >>sys.stderr, """\
Usage: %s [ -o <recipient> ... ] [-s <sender> ... ] [ -l <socket> | -L ] \\
         [ -T <store type ] \\
         [ -S <store directory> ] [ -P <publishing directory> ] \\
         [ -p <preferences directory> ] [ -j <journal directory> ] [ -d ]

Address options:

-o  Indicate the original recipients of the message, overriding any found in
    the message headers
-s  Indicate the senders of the message, overriding any found in the message
    headers

Delivery options:

-l  The socket filename for LMTP communication with a mailbox solution,
    selecting the LMTP delivery method
-L  Selects the local SMTP delivery method, requiring a suitable mail system
    configuration

(Where a program needs to deliver messages, one of the above options must be
specified.)

Configuration options:

-j  Indicates the location of quota-related journal information
-P  Indicates the location of published free/busy resources
-p  Indicates the location of user preference directories
-S  Indicates the location of the calendar data store containing user storage
    directories
-T  Indicates the store and journal type (the configured value if omitted)

Output options:

-d  Run in debug mode, producing informative output describing the behaviour
    of the program
""" % os.path.split(sys.argv[0])[-1]
        elif "-d" in args:
            self.process_args(args, sys.stdin)
        else:
            try:
                self.process_args(args, sys.stdin)
            except SystemExit, value:
                sys.exit(value)
            except Exception, exc:
                if "-v" in args:
                    raise
                type, value, tb = sys.exc_info()
                while tb.tb_next:
                    tb = tb.tb_next
                f = tb.tb_frame
                co = f and f.f_code
                filename = co and co.co_filename
                print >>sys.stderr, "Exception %s at %d in %s" % (exc, tb.tb_lineno, filename)
                #import traceback
                #traceback.print_exc(file=open("/tmp/mail.log", "a"))
                sys.exit(EX_TEMPFAIL)
        sys.exit(0)

class Recipient(Client):

    "A processor acting as a client on behalf of a recipient."

    def __init__(self, user, messenger, store, publisher, journal, preferences_dir,
                 handlers, outgoing_only, debug):

        """
        Initialise the recipient with the given 'user' identity, 'messenger',
        'store', 'publisher', 'journal', 'preferences_dir', 'handlers',
        'outgoing_only' and 'debug' status.
        """

        Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir)
        self.handlers = handlers
        self.outgoing_only = outgoing_only
        self.debug = debug

    def process(self, msg, senders):

        """
        Process the given 'msg' for a single recipient, having the given
        'senders'.

        Processing individually means that contributions to resulting messages
        may be constructed according to individual preferences.
        """

        handlers = dict([(name, cls(senders, self.user and get_address(self.user),
                                    self.messenger, self.store, self.publisher,
                                    self.journal, self.preferences_dir))
                         for name, cls in self.handlers])
        handled = False

        # Check for participating recipients. Non-participating recipients will
        # have their messages left as being unhandled.

        if self.outgoing_only or self.is_participating():

            # Check for returned messages.

            for part in msg.walk():
                if part.get_content_type() == "message/delivery-status":
                    break
            else:
                for part in msg.walk():
                    if part.get_content_type() in itip_content_types and \
                       part.get_param("method"):

                        handle_itip_part(part, handlers)
                        handled = True

        # When processing outgoing messages, no replies or deliveries are
        # performed.

        if self.outgoing_only:
            return

        # Get responses from the handlers.

        all_responses = []
        for handler in handlers.values():
            all_responses += handler.get_results()

        # Pack any returned parts into messages.

        if all_responses:
            outgoing_parts = {}
            forwarded_parts = []

            for outgoing_recipients, part in all_responses:
                if outgoing_recipients:
                    for outgoing_recipient in outgoing_recipients:
                        if not outgoing_parts.has_key(outgoing_recipient):
                            outgoing_parts[outgoing_recipient] = []
                        outgoing_parts[outgoing_recipient].append(part)
                else:
                    forwarded_parts.append(part)

            # Reply using any outgoing parts in a new message.

            if outgoing_parts:

                # Obtain free/busy details, if configured to do so.

                fb = self.can_provide_freebusy(handlers) and self.get_freebusy_part()

                for outgoing_recipient, parts in outgoing_parts.items():

                    # Bundle free/busy messages, if configured to do so.

                    if fb: parts.append(fb)
                    message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])

                    if self.debug:
                        print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient
                        print message
                    else:
                        self.messenger.sendmail([outgoing_recipient], message.as_string())

            # Forward messages to their recipients either wrapping the existing
            # message, accompanying it or replacing it.

            if forwarded_parts:

                # Determine whether to wrap, accompany or replace the message.

                prefs = self.get_preferences()
                incoming = prefs.get("incoming", settings["INCOMING_DEFAULT"])

                if incoming == "message-only":
                    messages = [msg]
                else:
                    summary = self.messenger.make_summary_message(msg, forwarded_parts)
                    if incoming == "summary-then-message":
                        messages = [summary, msg]
                    elif incoming == "message-then-summary":
                        messages = [msg, summary]
                    elif incoming == "summary-only":
                        messages = [summary]
                    else: # incoming == "summary-wraps-message":
                        messages = [self.messenger.wrap_message(msg, forwarded_parts)]

                for message in messages:
                    if self.debug:
                        print >>sys.stderr, "Forwarded parts..."
                        print message
                    elif self.messenger.local_delivery():
                        self.messenger.sendmail([get_address(self.user)], message.as_string())

        # Unhandled messages are delivered as they are.

        if not handled:
            if self.debug:
                print >>sys.stderr, "Unhandled parts..."
                print msg
            elif self.messenger.local_delivery():
                self.messenger.sendmail([get_address(self.user)], msg.as_string())

    def can_provide_freebusy(self, handlers):

        "Test for any free/busy information produced by 'handlers'."

        fbhandler = handlers.get("VFREEBUSY")
        if fbhandler:
            fbmethods = fbhandler.get_outgoing_methods()
            return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods
        else:
            return False

# vim: tabstop=4 expandtab shiftwidth=4