1 # -*- test-case-name: twisted.mail.test.test_mail -*-
3 # Copyright (c) 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))