~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/mail/alias.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.mail.test.test_mail -*-
 
2
#
 
3
# Copyright (c) 2001-2007 Twisted Matrix Laboratories.
 
4
# See LICENSE for details.
 
5
 
 
6
 
 
7
"""
 
8
Support for aliases(5) configuration files
 
9
 
 
10
@author: Jp Calderone
 
11
 
 
12
TODO::
 
13
    Monitor files for reparsing
 
14
    Handle non-local alias targets
 
15
    Handle maildir alias targets
 
16
"""
 
17
 
 
18
import os
 
19
import tempfile
 
20
 
 
21
from twisted.mail import smtp
 
22
from twisted.internet import reactor
 
23
from twisted.internet import protocol
 
24
from twisted.internet import defer
 
25
from twisted.python import failure
 
26
from twisted.python import log
 
27
from zope.interface import implements, Interface
 
28
 
 
29
 
 
30
def handle(result, line, filename, lineNo):
 
31
    parts = [p.strip() for p in line.split(':', 1)]
 
32
    if len(parts) != 2:
 
33
        fmt = "Invalid format on line %d of alias file %s."
 
34
        arg = (lineNo, filename)
 
35
        log.err(fmt % arg)
 
36
    else:
 
37
        user, alias = parts
 
38
        result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',')))
 
39
 
 
40
def loadAliasFile(domains, filename=None, fp=None):
 
41
    """Load a file containing email aliases.
 
42
 
 
43
    Lines in the file should be formatted like so::
 
44
 
 
45
        username: alias1,alias2,...,aliasN
 
46
 
 
47
    Aliases beginning with a | will be treated as programs, will be run, and
 
48
    the message will be written to their stdin.
 
49
 
 
50
    Aliases without a host part will be assumed to be addresses on localhost.
 
51
 
 
52
    If a username is specified multiple times, the aliases for each are joined
 
53
    together as if they had all been on one line.
 
54
 
 
55
    @type domains: C{dict} of implementor of C{IDomain}
 
56
    @param domains: The domains to which these aliases will belong.
 
57
 
 
58
    @type filename: C{str}
 
59
    @param filename: The filename from which to load aliases.
 
60
 
 
61
    @type fp: Any file-like object.
 
62
    @param fp: If specified, overrides C{filename}, and aliases are read from
 
63
    it.
 
64
 
 
65
    @rtype: C{dict}
 
66
    @return: A dictionary mapping usernames to C{AliasGroup} objects.
 
67
    """
 
68
    result = {}
 
69
    if fp is None:
 
70
        fp = file(filename)
 
71
    else:
 
72
        filename = getattr(fp, 'name', '<unknown>')
 
73
    i = 0
 
74
    prev = ''
 
75
    for line in fp:
 
76
        i += 1
 
77
        line = line.rstrip()
 
78
        if line.lstrip().startswith('#'):
 
79
            continue
 
80
        elif line.startswith(' ') or line.startswith('\t'):
 
81
            prev = prev + line
 
82
        else:
 
83
            if prev:
 
84
                handle(result, prev, filename, i)
 
85
            prev = line
 
86
    if prev:
 
87
        handle(result, prev, filename, i)
 
88
    for (u, a) in result.items():
 
89
        addr = smtp.Address(u)
 
90
        result[u] = AliasGroup(a, domains, u)
 
91
    return result
 
92
 
 
93
class IAlias(Interface):
 
94
    def createMessageReceiver():
 
95
        pass
 
96
 
 
97
class AliasBase:
 
98
    def __init__(self, domains, original):
 
99
        self.domains = domains
 
100
        self.original = smtp.Address(original)
 
101
 
 
102
    def domain(self):
 
103
        return self.domains[self.original.domain]
 
104
 
 
105
    def resolve(self, aliasmap, memo=None):
 
106
        if memo is None:
 
107
            memo = {}
 
108
        if str(self) in memo:
 
109
            return None
 
110
        memo[str(self)] = None
 
111
        return self.createMessageReceiver()
 
112
 
 
113
class AddressAlias(AliasBase):
 
114
    """The simplest alias, translating one email address into another."""
 
115
 
 
116
    implements(IAlias)
 
117
 
 
118
    def __init__(self, alias, *args):
 
119
        AliasBase.__init__(self, *args)
 
120
        self.alias = smtp.Address(alias)
 
121
 
 
122
    def __str__(self):
 
123
        return '<Address %s>' % (self.alias,)
 
124
 
 
125
    def createMessageReceiver(self):
 
126
        return self.domain().startMessage(str(self.alias))
 
127
 
 
128
    def resolve(self, aliasmap, memo=None):
 
129
        if memo is None:
 
130
            memo = {}
 
131
        if str(self) in memo:
 
132
            return None
 
133
        memo[str(self)] = None
 
134
        try:
 
135
            return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
 
136
        except smtp.SMTPBadRcpt:
 
137
            pass
 
138
        if self.alias.local in aliasmap:
 
139
            return aliasmap[self.alias.local].resolve(aliasmap, memo)
 
140
        return None
 
141
 
 
142
class FileWrapper:
 
143
    implements(smtp.IMessage)
 
144
 
 
145
    def __init__(self, filename):
 
146
        self.fp = tempfile.TemporaryFile()
 
147
        self.finalname = filename
 
148
 
 
149
    def lineReceived(self, line):
 
150
        self.fp.write(line + '\n')
 
151
 
 
152
    def eomReceived(self):
 
153
        self.fp.seek(0, 0)
 
154
        try:
 
155
            f = file(self.finalname, 'a')
 
156
        except:
 
157
            return defer.fail(failure.Failure())
 
158
 
 
159
        f.write(self.fp.read())
 
160
        self.fp.close()
 
161
        f.close()
 
162
 
 
163
        return defer.succeed(self.finalname)
 
164
 
 
165
    def connectionLost(self):
 
166
        self.fp.close()
 
167
        self.fp = None
 
168
 
 
169
    def __str__(self):
 
170
        return '<FileWrapper %s>' % (self.finalname,)
 
171
 
 
172
 
 
173
class FileAlias(AliasBase):
 
174
 
 
175
    implements(IAlias)
 
176
 
 
177
    def __init__(self, filename, *args):
 
178
        AliasBase.__init__(self, *args)
 
179
        self.filename = filename
 
180
 
 
181
    def __str__(self):
 
182
        return '<File %s>' % (self.filename,)
 
183
 
 
184
    def createMessageReceiver(self):
 
185
        return FileWrapper(self.filename)
 
186
 
 
187
 
 
188
 
 
189
class ProcessAliasTimeout(Exception):
 
190
    """
 
191
    A timeout occurred while processing aliases.
 
192
    """
 
193
 
 
194
 
 
195
 
 
196
class MessageWrapper:
 
197
    """
 
198
    A message receiver which delivers content to a child process.
 
199
 
 
200
    @type completionTimeout: C{int} or C{float}
 
201
    @ivar completionTimeout: The number of seconds to wait for the child
 
202
        process to exit before reporting the delivery as a failure.
 
203
 
 
204
    @type _timeoutCallID: C{NoneType} or L{IDelayedCall}
 
205
    @ivar _timeoutCallID: The call used to time out delivery, started when the
 
206
        connection to the child process is closed.
 
207
 
 
208
    @type done: C{bool}
 
209
    @ivar done: Flag indicating whether the child process has exited or not.
 
210
 
 
211
    @ivar reactor: An L{IReactorTime} provider which will be used to schedule
 
212
        timeouts.
 
213
    """
 
214
    implements(smtp.IMessage)
 
215
 
 
216
    done = False
 
217
 
 
218
    completionTimeout = 60
 
219
    _timeoutCallID = None
 
220
 
 
221
    reactor = reactor
 
222
 
 
223
    def __init__(self, protocol, process=None, reactor=None):
 
224
        self.processName = process
 
225
        self.protocol = protocol
 
226
        self.completion = defer.Deferred()
 
227
        self.protocol.onEnd = self.completion
 
228
        self.completion.addBoth(self._processEnded)
 
229
 
 
230
        if reactor is not None:
 
231
            self.reactor = reactor
 
232
 
 
233
 
 
234
    def _processEnded(self, result):
 
235
        """
 
236
        Record process termination and cancel the timeout call if it is active.
 
237
        """
 
238
        self.done = True
 
239
        if self._timeoutCallID is not None:
 
240
            # eomReceived was called, we're actually waiting for the process to
 
241
            # exit.
 
242
            self._timeoutCallID.cancel()
 
243
            self._timeoutCallID = None
 
244
        else:
 
245
            # eomReceived was not called, this is unexpected, propagate the
 
246
            # error.
 
247
            return result
 
248
 
 
249
 
 
250
    def lineReceived(self, line):
 
251
        if self.done:
 
252
            return
 
253
        self.protocol.transport.write(line + '\n')
 
254
 
 
255
 
 
256
    def eomReceived(self):
 
257
        """
 
258
        Disconnect from the child process, set up a timeout to wait for it to
 
259
        exit, and return a Deferred which will be called back when the child
 
260
        process exits.
 
261
        """
 
262
        if not self.done:
 
263
            self.protocol.transport.loseConnection()
 
264
            self._timeoutCallID = self.reactor.callLater(
 
265
                self.completionTimeout, self._completionCancel)
 
266
        return self.completion
 
267
 
 
268
 
 
269
    def _completionCancel(self):
 
270
        """
 
271
        Handle the expiration of the timeout for the child process to exit by
 
272
        terminating the child process forcefully and issuing a failure to the
 
273
        completion deferred returned by L{eomReceived}.
 
274
        """
 
275
        self._timeoutCallID = None
 
276
        self.protocol.transport.signalProcess('KILL')
 
277
        exc = ProcessAliasTimeout(
 
278
            "No answer after %s seconds" % (self.completionTimeout,))
 
279
        self.protocol.onEnd = None
 
280
        self.completion.errback(failure.Failure(exc))
 
281
 
 
282
 
 
283
    def connectionLost(self):
 
284
        # Heh heh
 
285
        pass
 
286
 
 
287
 
 
288
    def __str__(self):
 
289
        return '<ProcessWrapper %s>' % (self.processName,)
 
290
 
 
291
 
 
292
 
 
293
class ProcessAliasProtocol(protocol.ProcessProtocol):
 
294
    """
 
295
    Trivial process protocol which will callback a Deferred when the associated
 
296
    process ends.
 
297
 
 
298
    @ivar onEnd: If not C{None}, a L{Deferred} which will be called back with
 
299
        the failure passed to C{processEnded}, when C{processEnded} is called.
 
300
    """
 
301
 
 
302
    onEnd = None
 
303
 
 
304
    def processEnded(self, reason):
 
305
        """
 
306
        Call back C{onEnd} if it is set.
 
307
        """
 
308
        if self.onEnd is not None:
 
309
            self.onEnd.errback(reason)
 
310
 
 
311
 
 
312
 
 
313
class ProcessAlias(AliasBase):
 
314
    """
 
315
    An alias which is handled by the execution of a particular program.
 
316
 
 
317
    @ivar reactor: An L{IReactorProcess} and L{IReactorTime} provider which
 
318
        will be used to create and timeout the alias child process.
 
319
    """
 
320
    implements(IAlias)
 
321
 
 
322
    reactor = reactor
 
323
 
 
324
    def __init__(self, path, *args):
 
325
        AliasBase.__init__(self, *args)
 
326
        self.path = path.split()
 
327
        self.program = self.path[0]
 
328
 
 
329
 
 
330
    def __str__(self):
 
331
        """
 
332
        Build a string representation containing the path.
 
333
        """
 
334
        return '<Process %s>' % (self.path,)
 
335
 
 
336
 
 
337
    def spawnProcess(self, proto, program, path):
 
338
        """
 
339
        Wrapper around C{reactor.spawnProcess}, to be customized for tests
 
340
        purpose.
 
341
        """
 
342
        return self.reactor.spawnProcess(proto, program, path)
 
343
 
 
344
 
 
345
    def createMessageReceiver(self):
 
346
        """
 
347
        Create a message receiver by launching a process.
 
348
        """
 
349
        p = ProcessAliasProtocol()
 
350
        m = MessageWrapper(p, self.program, self.reactor)
 
351
        fd = self.spawnProcess(p, self.program, self.path)
 
352
        return m
 
353
 
 
354
 
 
355
 
 
356
class MultiWrapper:
 
357
    """
 
358
    Wrapper to deliver a single message to multiple recipients.
 
359
    """
 
360
 
 
361
    implements(smtp.IMessage)
 
362
 
 
363
    def __init__(self, objs):
 
364
        self.objs = objs
 
365
 
 
366
    def lineReceived(self, line):
 
367
        for o in self.objs:
 
368
            o.lineReceived(line)
 
369
 
 
370
    def eomReceived(self):
 
371
        return defer.DeferredList([
 
372
            o.eomReceived() for o in self.objs
 
373
        ])
 
374
 
 
375
    def connectionLost(self):
 
376
        for o in self.objs:
 
377
            o.connectionLost()
 
378
 
 
379
    def __str__(self):
 
380
        return '<GroupWrapper %r>' % (map(str, self.objs),)
 
381
 
 
382
 
 
383
 
 
384
class AliasGroup(AliasBase):
 
385
    """
 
386
    An alias which points to more than one recipient.
 
387
 
 
388
    @ivar processAliasFactory: a factory for resolving process aliases.
 
389
    @type processAliasFactory: C{class}
 
390
    """
 
391
 
 
392
    implements(IAlias)
 
393
 
 
394
    processAliasFactory = ProcessAlias
 
395
 
 
396
    def __init__(self, items, *args):
 
397
        AliasBase.__init__(self, *args)
 
398
        self.aliases = []
 
399
        while items:
 
400
            addr = items.pop().strip()
 
401
            if addr.startswith(':'):
 
402
                try:
 
403
                    f = file(addr[1:])
 
404
                except:
 
405
                    log.err("Invalid filename in alias file %r" % (addr[1:],))
 
406
                else:
 
407
                    addr = ' '.join([l.strip() for l in f])
 
408
                    items.extend(addr.split(','))
 
409
            elif addr.startswith('|'):
 
410
                self.aliases.append(self.processAliasFactory(addr[1:], *args))
 
411
            elif addr.startswith('/'):
 
412
                if os.path.isdir(addr):
 
413
                    log.err("Directory delivery not supported")
 
414
                else:
 
415
                    self.aliases.append(FileAlias(addr, *args))
 
416
            else:
 
417
                self.aliases.append(AddressAlias(addr, *args))
 
418
 
 
419
    def __len__(self):
 
420
        return len(self.aliases)
 
421
 
 
422
    def __str__(self):
 
423
        return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
 
424
 
 
425
    def createMessageReceiver(self):
 
426
        return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
 
427
 
 
428
    def resolve(self, aliasmap, memo=None):
 
429
        if memo is None:
 
430
            memo = {}
 
431
        r = []
 
432
        for a in self.aliases:
 
433
            r.append(a.resolve(aliasmap, memo))
 
434
        return MultiWrapper(filter(None, r))
 
435