1# -*- test-case-name: twisted.mail.test.test_mail -*-
2#
3# Copyright (c) Twisted Matrix Laboratories.
4# See LICENSE for details.
5
6
7"""
8Support for aliases(5) configuration files.
9
10@author: Jp Calderone
11"""
12
13import os
14import tempfile
15
16from twisted.mail import smtp
17from twisted.internet import reactor
18from twisted.internet import protocol
19from twisted.internet import defer
20from twisted.python import failure
21from twisted.python import log
22from zope.interface import implements, Interface
23
24
25def handle(result, line, filename, lineNo):
26    """
27    Parse a line from an aliases file.
28
29    @type result: L{dict} mapping L{bytes} to L{list} of L{bytes}
30    @param result: A dictionary mapping username to aliases to which
31        the results of parsing the line are added.
32
33    @type line: L{bytes}
34    @param line: A line from an aliases file.
35
36    @type filename: L{bytes}
37    @param filename: The full or relative path to the aliases file.
38
39    @type lineNo: L{int}
40    @param lineNo: The position of the line within the aliases file.
41    """
42    parts = [p.strip() for p in line.split(':', 1)]
43    if len(parts) != 2:
44        fmt = "Invalid format on line %d of alias file %s."
45        arg = (lineNo, filename)
46        log.err(fmt % arg)
47    else:
48        user, alias = parts
49        result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',')))
50
51
52
53def loadAliasFile(domains, filename=None, fp=None):
54    """
55    Load a file containing email aliases.
56
57    Lines in the file should be formatted like so::
58
59         username: alias1, alias2, ..., aliasN
60
61    Aliases beginning with a C{|} will be treated as programs, will be run, and
62    the message will be written to their stdin.
63
64    Aliases beginning with a C{:} will be treated as a file containing
65    additional aliases for the username.
66
67    Aliases beginning with a C{/} will be treated as the full pathname to a file
68    to which the message will be appended.
69
70    Aliases without a host part will be assumed to be addresses on localhost.
71
72    If a username is specified multiple times, the aliases for each are joined
73    together as if they had all been on one line.
74
75    Lines beginning with a space or a tab are continuations of the previous
76    line.
77
78    Lines beginning with a C{#} are comments.
79
80    @type domains: L{dict} mapping L{bytes} to L{IDomain} provider
81    @param domains: A mapping of domain name to domain object.
82
83    @type filename: L{bytes} or L{NoneType <types.NoneType>}
84    @param filename: The full or relative path to a file from which to load
85        aliases. If omitted, the C{fp} parameter must be specified.
86
87    @type fp: file-like object or L{NoneType <types.NoneType>}
88    @param fp: The file from which to load aliases. If specified,
89        the C{filename} parameter is ignored.
90
91    @rtype: L{dict} mapping L{bytes} to L{AliasGroup}
92    @return: A mapping from username to group of aliases.
93    """
94    result = {}
95    if fp is None:
96        fp = file(filename)
97    else:
98        filename = getattr(fp, 'name', '<unknown>')
99    i = 0
100    prev = ''
101    for line in fp:
102        i += 1
103        line = line.rstrip()
104        if line.lstrip().startswith('#'):
105            continue
106        elif line.startswith(' ') or line.startswith('\t'):
107            prev = prev + line
108        else:
109            if prev:
110                handle(result, prev, filename, i)
111            prev = line
112    if prev:
113        handle(result, prev, filename, i)
114    for (u, a) in result.items():
115        addr = smtp.Address(u)
116        result[u] = AliasGroup(a, domains, u)
117    return result
118
119
120
121class IAlias(Interface):
122    """
123    An interface for aliases.
124    """
125    def createMessageReceiver():
126        """
127        Create a message receiver.
128
129        @rtype: L{IMessage <smtp.IMessage>} provider
130        @return: A message receiver.
131        """
132
133
134
135class AliasBase:
136    """
137    The default base class for aliases.
138
139    @ivar domains: See L{__init__}.
140
141    @type original: L{Address}
142    @ivar original: The original address being aliased.
143    """
144    def __init__(self, domains, original):
145        """
146        @type domains: L{dict} mapping L{bytes} to L{IDomain} provider
147        @param domains: A mapping of domain name to domain object.
148
149        @type original: L{bytes}
150        @param original: The original address being aliased.
151        """
152        self.domains = domains
153        self.original = smtp.Address(original)
154
155
156    def domain(self):
157        """
158        Return the domain associated with original address.
159
160        @rtype: L{IDomain} provider
161        @return: The domain for the original address.
162        """
163        return self.domains[self.original.domain]
164
165
166    def resolve(self, aliasmap, memo=None):
167        """
168        Map this alias to its ultimate destination.
169
170        @type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
171        @param aliasmap: A mapping of username to alias or group of aliases.
172
173        @type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
174        @param memo: A record of the aliases already considered in the
175            resolution process.  If provided, C{memo} is modified to include
176            this alias.
177
178        @rtype: L{IMessage <smtp.IMessage>} or L{NoneType <types.NoneType>}
179        @return: A message receiver for the ultimate destination or None for
180            an invalid destination.
181        """
182        if memo is None:
183            memo = {}
184        if str(self) in memo:
185            return None
186        memo[str(self)] = None
187        return self.createMessageReceiver()
188
189
190
191class AddressAlias(AliasBase):
192    """
193    An alias which translates one email address into another.
194
195    @type alias : L{Address}
196    @ivar alias: The destination address.
197    """
198    implements(IAlias)
199
200    def __init__(self, alias, *args):
201        """
202        @type alias: L{Address}, L{User}, L{bytes} or object which can be
203            converted into L{bytes}
204        @param alias: The destination address.
205
206        @type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
207            provider, (1) L{bytes}
208        @param args: Arguments for L{AliasBase.__init__}.
209        """
210        AliasBase.__init__(self, *args)
211        self.alias = smtp.Address(alias)
212
213
214    def __str__(self):
215        """
216        Build a string representation of this L{AddressAlias} instance.
217
218        @rtype: L{bytes}
219        @return: A string containing the destination address.
220        """
221        return '<Address %s>' % (self.alias,)
222
223
224    def createMessageReceiver(self):
225        """
226        Create a message receiver which delivers a message to
227        the destination address.
228
229        @rtype: L{IMessage <smtp.IMessage>} provider
230        @return: A message receiver.
231        """
232        return self.domain().exists(str(self.alias))
233
234
235    def resolve(self, aliasmap, memo=None):
236        """
237        Map this alias to its ultimate destination.
238
239        @type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
240        @param aliasmap: A mapping of username to alias or group of aliases.
241
242        @type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
243        @param memo: A record of the aliases already considered in the
244            resolution process.  If provided, C{memo} is modified to include
245            this alias.
246
247        @rtype: L{IMessage <smtp.IMessage>} or L{NoneType <types.NoneType>}
248        @return: A message receiver for the ultimate destination or None for
249            an invalid destination.
250        """
251        if memo is None:
252            memo = {}
253        if str(self) in memo:
254            return None
255        memo[str(self)] = None
256        try:
257            return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
258        except smtp.SMTPBadRcpt:
259            pass
260        if self.alias.local in aliasmap:
261            return aliasmap[self.alias.local].resolve(aliasmap, memo)
262        return None
263
264
265
266class FileWrapper:
267    """
268    A message receiver which delivers a message to a file.
269
270    @type fp: file-like object
271    @ivar fp: A file used for temporary storage of the message.
272
273    @type finalname: L{bytes}
274    @ivar finalname: The name of the file in which the message should be
275        stored.
276    """
277    implements(smtp.IMessage)
278
279    def __init__(self, filename):
280        """
281        @type filename: L{bytes}
282        @param filename: The name of the file in which the message should be
283            stored.
284        """
285        self.fp = tempfile.TemporaryFile()
286        self.finalname = filename
287
288
289    def lineReceived(self, line):
290        """
291        Write a received line to the temporary file.
292
293        @type line: L{bytes}
294        @param line: A received line of the message.
295        """
296        self.fp.write(line + '\n')
297
298
299    def eomReceived(self):
300        """
301        Handle end of message by writing the message to the file.
302
303        @rtype: L{Deferred <defer.Deferred>} which successfully results in
304            L{bytes}
305        @return: A deferred which succeeds with the name of the file to which
306            the message has been stored or fails if the message cannot be
307            saved to the file.
308        """
309        self.fp.seek(0, 0)
310        try:
311            f = file(self.finalname, 'a')
312        except:
313            return defer.fail(failure.Failure())
314
315        f.write(self.fp.read())
316        self.fp.close()
317        f.close()
318
319        return defer.succeed(self.finalname)
320
321
322    def connectionLost(self):
323        """
324        Close the temporary file when the connection is lost.
325        """
326        self.fp.close()
327        self.fp = None
328
329
330    def __str__(self):
331        """
332        Build a string representation of this L{FileWrapper} instance.
333
334        @rtype: L{bytes}
335        @return: A string containing the file name of the message.
336        """
337        return '<FileWrapper %s>' % (self.finalname,)
338
339
340
341class FileAlias(AliasBase):
342    """
343    An alias which translates an address to a file.
344
345    @ivar filename: See L{__init__}.
346    """
347    implements(IAlias)
348
349    def __init__(self, filename, *args):
350        """
351        @type filename: L{bytes}
352        @param filename: The name of the file in which to store the message.
353
354        @type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
355            provider, (1) L{bytes}
356        @param args: Arguments for L{AliasBase.__init__}.
357        """
358        AliasBase.__init__(self, *args)
359        self.filename = filename
360
361
362    def __str__(self):
363        """
364        Build a string representation of this L{FileAlias} instance.
365
366        @rtype: L{bytes}
367        @return: A string containing the name of the file.
368        """
369        return '<File %s>' % (self.filename,)
370
371
372    def createMessageReceiver(self):
373        """
374        Create a message receiver which delivers a message to the file.
375
376        @rtype: L{FileWrapper}
377        @return: A message receiver which writes a message to the file.
378        """
379        return FileWrapper(self.filename)
380
381
382
383class ProcessAliasTimeout(Exception):
384    """
385    An error indicating that a timeout occurred while waiting for a process
386    to complete.
387    """
388
389
390
391class MessageWrapper:
392    """
393    A message receiver which delivers a message to a child process.
394
395    @type completionTimeout: L{int} or L{float}
396    @ivar completionTimeout: The number of seconds to wait for the child
397        process to exit before reporting the delivery as a failure.
398
399    @type _timeoutCallID: L{NoneType <types.NoneType>} or
400        L{IDelayedCall <twisted.internet.interfaces.IDelayedCall>} provider
401    @ivar _timeoutCallID: The call used to time out delivery, started when the
402        connection to the child process is closed.
403
404    @type done: L{bool}
405    @ivar done: A flag indicating whether the child process has exited
406        (C{True}) or not (C{False}).
407
408    @type reactor: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
409        provider
410    @ivar reactor: A reactor which will be used to schedule timeouts.
411
412    @ivar protocol: See L{__init__}.
413
414    @type processName: L{bytes} or L{NoneType <types.NoneType>}
415    @ivar processName: The process name.
416
417    @type completion: L{Deferred <defer.Deferred>}
418    @ivar completion: The deferred which will be triggered by the protocol
419        when the child process exits.
420    """
421    implements(smtp.IMessage)
422
423    done = False
424
425    completionTimeout = 60
426    _timeoutCallID = None
427
428    reactor = reactor
429
430    def __init__(self, protocol, process=None, reactor=None):
431        """
432        @type protocol: L{ProcessAliasProtocol}
433        @param protocol: The protocol associated with the child process.
434
435        @type process: L{bytes} or L{NoneType <types.NoneType>}
436        @param process: The process name.
437
438        @type reactor: L{NoneType <types.NoneType>} or L{IReactorTime
439            <twisted.internet.interfaces.IReactorTime>} provider
440        @param reactor: A reactor which will be used to schedule timeouts.
441        """
442        self.processName = process
443        self.protocol = protocol
444        self.completion = defer.Deferred()
445        self.protocol.onEnd = self.completion
446        self.completion.addBoth(self._processEnded)
447
448        if reactor is not None:
449            self.reactor = reactor
450
451
452    def _processEnded(self, result):
453        """
454        Record process termination and cancel the timeout call if it is active.
455
456        @type result: L{Failure <failure.Failure>}
457        @param result: The reason the child process terminated.
458
459        @rtype: L{NoneType <types.NoneType>} or
460            L{Failure <failure.Failure>}
461        @return: None, if the process end is expected, or the reason the child
462            process terminated, if the process end is unexpected.
463        """
464        self.done = True
465        if self._timeoutCallID is not None:
466            # eomReceived was called, we're actually waiting for the process to
467            # exit.
468            self._timeoutCallID.cancel()
469            self._timeoutCallID = None
470        else:
471            # eomReceived was not called, this is unexpected, propagate the
472            # error.
473            return result
474
475
476    def lineReceived(self, line):
477        """
478        Write a received line to the child process.
479
480        @type line: L{bytes}
481        @param line: A received line of the message.
482        """
483        if self.done:
484            return
485        self.protocol.transport.write(line + '\n')
486
487
488    def eomReceived(self):
489        """
490        Disconnect from the child process and set up a timeout to wait for it
491        to exit.
492
493        @rtype: L{Deferred <defer.Deferred>}
494        @return: A deferred which will be called back when the child process
495            exits.
496        """
497        if not self.done:
498            self.protocol.transport.loseConnection()
499            self._timeoutCallID = self.reactor.callLater(
500                self.completionTimeout, self._completionCancel)
501        return self.completion
502
503
504    def _completionCancel(self):
505        """
506        Handle the expiration of the timeout for the child process to exit by
507        terminating the child process forcefully and issuing a failure to the
508        L{completion} deferred.
509        """
510        self._timeoutCallID = None
511        self.protocol.transport.signalProcess('KILL')
512        exc = ProcessAliasTimeout(
513            "No answer after %s seconds" % (self.completionTimeout,))
514        self.protocol.onEnd = None
515        self.completion.errback(failure.Failure(exc))
516
517
518    def connectionLost(self):
519        """
520        Ignore notification of lost connection.
521        """
522
523
524    def __str__(self):
525        """
526        Build a string representation of this L{MessageWrapper} instance.
527
528        @rtype: L{bytes}
529        @return: A string containing the name of the process.
530        """
531        return '<ProcessWrapper %s>' % (self.processName,)
532
533
534
535class ProcessAliasProtocol(protocol.ProcessProtocol):
536    """
537    A process protocol which errbacks a deferred when the associated
538    process ends.
539
540    @type onEnd: L{NoneType <types.NoneType>} or L{Deferred <defer.Deferred>}
541    @ivar onEnd: If set, a deferred on which to errback when the process ends.
542    """
543    onEnd = None
544
545    def processEnded(self, reason):
546        """
547        Call an errback.
548
549        @type reason: L{Failure <failure.Failure>}
550        @param reason: The reason the child process terminated.
551        """
552        if self.onEnd is not None:
553            self.onEnd.errback(reason)
554
555
556
557class ProcessAlias(AliasBase):
558    """
559    An alias which is handled by the execution of a program.
560
561    @type path: L{list} of L{bytes}
562    @ivar path: The arguments to pass to the process. The first string is
563        the executable's name.
564
565    @type program: L{bytes}
566    @ivar program: The path of the program to be executed.
567
568    @type reactor: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
569        and L{IReactorProcess <twisted.internet.interfaces.IReactorProcess>}
570        provider
571    @ivar reactor: A reactor which will be used to create and timeout the
572        child process.
573    """
574    implements(IAlias)
575
576    reactor = reactor
577
578    def __init__(self, path, *args):
579        """
580        @type path: L{bytes}
581        @param path: The command to invoke the program consisting of the path
582            to the executable followed by any arguments.
583
584        @type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
585            provider, (1) L{bytes}
586        @param args: Arguments for L{AliasBase.__init__}.
587        """
588
589        AliasBase.__init__(self, *args)
590        self.path = path.split()
591        self.program = self.path[0]
592
593
594    def __str__(self):
595        """
596        Build a string representation of this L{ProcessAlias} instance.
597
598        @rtype: L{bytes}
599        @return: A string containing the command used to invoke the process.
600        """
601        return '<Process %s>' % (self.path,)
602
603
604    def spawnProcess(self, proto, program, path):
605        """
606        Spawn a process.
607
608        This wraps the L{spawnProcess
609        <twisted.internet.interfaces.IReactorProcess.spawnProcess>} method on
610        L{reactor} so that it can be customized for test purposes.
611
612        @type proto: L{IProcessProtocol
613            <twisted.internet.interfaces.IProcessProtocol>} provider
614        @param proto: An object which will be notified of all events related to
615            the created process.
616
617        @type program: L{bytes}
618        @param program: The full path name of the file to execute.
619
620        @type path: L{list} of L{bytes}
621        @param path: The arguments to pass to the process. The first string
622            should be the executable's name.
623
624        @rtype: L{IProcessTransport
625            <twisted.internet.interfaces.IProcessTransport>} provider
626        @return: A process transport.
627        """
628        return self.reactor.spawnProcess(proto, program, path)
629
630
631    def createMessageReceiver(self):
632        """
633        Launch a process and create a message receiver to pass a message
634        to the process.
635
636        @rtype: L{MessageWrapper}
637        @return: A message receiver which delivers a message to the process.
638        """
639        p = ProcessAliasProtocol()
640        m = MessageWrapper(p, self.program, self.reactor)
641        fd = self.spawnProcess(p, self.program, self.path)
642        return m
643
644
645
646class MultiWrapper:
647    """
648    A message receiver which delivers a single message to multiple other
649    message receivers.
650
651    @ivar objs: See L{__init__}.
652    """
653    implements(smtp.IMessage)
654
655    def __init__(self, objs):
656        """
657        @type objs: L{list} of L{IMessage <smtp.IMessage>} provider
658        @param objs: Message receivers to which the incoming message should be
659            directed.
660        """
661        self.objs = objs
662
663
664    def lineReceived(self, line):
665        """
666        Pass a received line to the message receivers.
667
668        @type line: L{bytes}
669        @param line: A line of the message.
670        """
671        for o in self.objs:
672            o.lineReceived(line)
673
674
675    def eomReceived(self):
676        """
677        Pass the end of message along to the message receivers.
678
679        @rtype: L{DeferredList <defer.DeferredList>} whose successful results
680            are L{bytes} or L{NoneType <types.NoneType>}
681        @return: A deferred list which triggers when all of the message
682            receivers have finished handling their end of message.
683        """
684        return defer.DeferredList([
685            o.eomReceived() for o in self.objs
686        ])
687
688
689    def connectionLost(self):
690        """
691        Inform the message receivers that the connection has been lost.
692        """
693        for o in self.objs:
694            o.connectionLost()
695
696
697    def __str__(self):
698        """
699        Build a string representation of this L{MultiWrapper} instance.
700
701        @rtype: L{bytes}
702        @return: A string containing a list of the message receivers.
703        """
704        return '<GroupWrapper %r>' % (map(str, self.objs),)
705
706
707
708class AliasGroup(AliasBase):
709    """
710    An alias which points to multiple destination aliases.
711
712    @type processAliasFactory: no-argument callable which returns
713        L{ProcessAlias}
714    @ivar processAliasFactory: A factory for process aliases.
715
716    @type aliases: L{list} of L{AliasBase} which implements L{IAlias}
717    @ivar aliases: The destination aliases.
718    """
719    implements(IAlias)
720
721    processAliasFactory = ProcessAlias
722
723    def __init__(self, items, *args):
724        """
725        Create a group of aliases.
726
727        Parse a list of alias strings and, for each, create an appropriate
728        alias object.
729
730        @type items: L{list} of L{bytes}
731        @param items: Aliases.
732
733        @type args: n-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
734            provider, (1) L{bytes}
735        @param args: Arguments for L{AliasBase.__init__}.
736        """
737
738        AliasBase.__init__(self, *args)
739        self.aliases = []
740        while items:
741            addr = items.pop().strip()
742            if addr.startswith(':'):
743                try:
744                    f = file(addr[1:])
745                except:
746                    log.err("Invalid filename in alias file %r" % (addr[1:],))
747                else:
748                    addr = ' '.join([l.strip() for l in f])
749                    items.extend(addr.split(','))
750            elif addr.startswith('|'):
751                self.aliases.append(self.processAliasFactory(addr[1:], *args))
752            elif addr.startswith('/'):
753                if os.path.isdir(addr):
754                    log.err("Directory delivery not supported")
755                else:
756                    self.aliases.append(FileAlias(addr, *args))
757            else:
758                self.aliases.append(AddressAlias(addr, *args))
759
760
761    def __len__(self):
762        """
763        Return the number of aliases in the group.
764
765        @rtype: L{int}
766        @return: The number of aliases in the group.
767        """
768        return len(self.aliases)
769
770
771    def __str__(self):
772        """
773        Build a string representation of this L{AliasGroup} instance.
774
775        @rtype: L{bytes}
776        @return: A string containing the aliases in the group.
777        """
778        return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
779
780
781    def createMessageReceiver(self):
782        """
783        Create a message receiver for each alias and return a message receiver
784        which will pass on a message to each of those.
785
786        @rtype: L{MultiWrapper}
787        @return: A message receiver which passes a message on to message
788            receivers for each alias in the group.
789        """
790        return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
791
792
793    def resolve(self, aliasmap, memo=None):
794        """
795        Map each of the aliases in the group to its ultimate destination.
796
797        @type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
798        @param aliasmap: A mapping of username to alias or group of aliases.
799
800        @type memo: L{NoneType <types.NoneType>} or L{dict} of L{AliasBase}
801        @param memo: A record of the aliases already considered in the
802            resolution process.  If provided, C{memo} is modified to include
803            this alias.
804
805        @rtype: L{MultiWrapper}
806        @return: A message receiver which passes the message on to message
807            receivers for the ultimate destination of each alias in the group.
808        """
809        if memo is None:
810            memo = {}
811        r = []
812        for a in self.aliases:
813            r.append(a.resolve(aliasmap, memo))
814        return MultiWrapper(filter(None, r))
815