Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / mail / alias.py
1 # -*- test-case-name: twisted.mail.test.test_mail -*-
2 #
3 # Copyright (c) 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