1
# -*- test-case-name: twisted.mail.test.test_mail -*-
3
# Copyright (c) 2001-2007 Twisted Matrix Laboratories.
4
# See LICENSE for details.
8
Support for aliases(5) configuration files
13
Monitor files for reparsing
14
Handle non-local alias targets
15
Handle maildir alias targets
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
30
def handle(result, line, filename, lineNo):
31
parts = [p.strip() for p in line.split(':', 1)]
33
fmt = "Invalid format on line %d of alias file %s."
34
arg = (lineNo, filename)
38
result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',')))
40
def loadAliasFile(domains, filename=None, fp=None):
41
"""Load a file containing email aliases.
43
Lines in the file should be formatted like so::
45
username: alias1,alias2,...,aliasN
47
Aliases beginning with a | will be treated as programs, will be run, and
48
the message will be written to their stdin.
50
Aliases without a host part will be assumed to be addresses on localhost.
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.
55
@type domains: C{dict} of implementor of C{IDomain}
56
@param domains: The domains to which these aliases will belong.
58
@type filename: C{str}
59
@param filename: The filename from which to load aliases.
61
@type fp: Any file-like object.
62
@param fp: If specified, overrides C{filename}, and aliases are read from
66
@return: A dictionary mapping usernames to C{AliasGroup} objects.
72
filename = getattr(fp, 'name', '<unknown>')
78
if line.lstrip().startswith('#'):
80
elif line.startswith(' ') or line.startswith('\t'):
84
handle(result, prev, filename, i)
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)
93
class IAlias(Interface):
94
def createMessageReceiver():
98
def __init__(self, domains, original):
99
self.domains = domains
100
self.original = smtp.Address(original)
103
return self.domains[self.original.domain]
105
def resolve(self, aliasmap, memo=None):
108
if str(self) in memo:
110
memo[str(self)] = None
111
return self.createMessageReceiver()
113
class AddressAlias(AliasBase):
114
"""The simplest alias, translating one email address into another."""
118
def __init__(self, alias, *args):
119
AliasBase.__init__(self, *args)
120
self.alias = smtp.Address(alias)
123
return '<Address %s>' % (self.alias,)
125
def createMessageReceiver(self):
126
return self.domain().startMessage(str(self.alias))
128
def resolve(self, aliasmap, memo=None):
131
if str(self) in memo:
133
memo[str(self)] = None
135
return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
136
except smtp.SMTPBadRcpt:
138
if self.alias.local in aliasmap:
139
return aliasmap[self.alias.local].resolve(aliasmap, memo)
143
implements(smtp.IMessage)
145
def __init__(self, filename):
146
self.fp = tempfile.TemporaryFile()
147
self.finalname = filename
149
def lineReceived(self, line):
150
self.fp.write(line + '\n')
152
def eomReceived(self):
155
f = file(self.finalname, 'a')
157
return defer.fail(failure.Failure())
159
f.write(self.fp.read())
163
return defer.succeed(self.finalname)
165
def connectionLost(self):
170
return '<FileWrapper %s>' % (self.finalname,)
173
class FileAlias(AliasBase):
177
def __init__(self, filename, *args):
178
AliasBase.__init__(self, *args)
179
self.filename = filename
182
return '<File %s>' % (self.filename,)
184
def createMessageReceiver(self):
185
return FileWrapper(self.filename)
189
class ProcessAliasTimeout(Exception):
191
A timeout occurred while processing aliases.
196
class MessageWrapper:
198
A message receiver which delivers content to a child process.
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.
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.
209
@ivar done: Flag indicating whether the child process has exited or not.
211
@ivar reactor: An L{IReactorTime} provider which will be used to schedule
214
implements(smtp.IMessage)
218
completionTimeout = 60
219
_timeoutCallID = None
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)
230
if reactor is not None:
231
self.reactor = reactor
234
def _processEnded(self, result):
236
Record process termination and cancel the timeout call if it is active.
239
if self._timeoutCallID is not None:
240
# eomReceived was called, we're actually waiting for the process to
242
self._timeoutCallID.cancel()
243
self._timeoutCallID = None
245
# eomReceived was not called, this is unexpected, propagate the
250
def lineReceived(self, line):
253
self.protocol.transport.write(line + '\n')
256
def eomReceived(self):
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
263
self.protocol.transport.loseConnection()
264
self._timeoutCallID = self.reactor.callLater(
265
self.completionTimeout, self._completionCancel)
266
return self.completion
269
def _completionCancel(self):
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}.
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))
283
def connectionLost(self):
289
return '<ProcessWrapper %s>' % (self.processName,)
293
class ProcessAliasProtocol(protocol.ProcessProtocol):
295
Trivial process protocol which will callback a Deferred when the associated
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.
304
def processEnded(self, reason):
306
Call back C{onEnd} if it is set.
308
if self.onEnd is not None:
309
self.onEnd.errback(reason)
313
class ProcessAlias(AliasBase):
315
An alias which is handled by the execution of a particular program.
317
@ivar reactor: An L{IReactorProcess} and L{IReactorTime} provider which
318
will be used to create and timeout the alias child process.
324
def __init__(self, path, *args):
325
AliasBase.__init__(self, *args)
326
self.path = path.split()
327
self.program = self.path[0]
332
Build a string representation containing the path.
334
return '<Process %s>' % (self.path,)
337
def spawnProcess(self, proto, program, path):
339
Wrapper around C{reactor.spawnProcess}, to be customized for tests
342
return self.reactor.spawnProcess(proto, program, path)
345
def createMessageReceiver(self):
347
Create a message receiver by launching a process.
349
p = ProcessAliasProtocol()
350
m = MessageWrapper(p, self.program, self.reactor)
351
fd = self.spawnProcess(p, self.program, self.path)
358
Wrapper to deliver a single message to multiple recipients.
361
implements(smtp.IMessage)
363
def __init__(self, objs):
366
def lineReceived(self, line):
370
def eomReceived(self):
371
return defer.DeferredList([
372
o.eomReceived() for o in self.objs
375
def connectionLost(self):
380
return '<GroupWrapper %r>' % (map(str, self.objs),)
384
class AliasGroup(AliasBase):
386
An alias which points to more than one recipient.
388
@ivar processAliasFactory: a factory for resolving process aliases.
389
@type processAliasFactory: C{class}
394
processAliasFactory = ProcessAlias
396
def __init__(self, items, *args):
397
AliasBase.__init__(self, *args)
400
addr = items.pop().strip()
401
if addr.startswith(':'):
405
log.err("Invalid filename in alias file %r" % (addr[1:],))
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")
415
self.aliases.append(FileAlias(addr, *args))
417
self.aliases.append(AddressAlias(addr, *args))
420
return len(self.aliases)
423
return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
425
def createMessageReceiver(self):
426
return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
428
def resolve(self, aliasmap, memo=None):
432
for a in self.aliases:
433
r.append(a.resolve(aliasmap, memo))
434
return MultiWrapper(filter(None, r))