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