1# -*- test-case-name: twisted.mail.test.test_options -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5
6"""
7Support for creating mail servers with twistd.
8"""
9
10import os
11
12from twisted.application import internet
13from twisted.cred import checkers, strcred
14from twisted.internet import endpoints
15from twisted.mail import alias, mail, maildir, relay, relaymanager
16from twisted.python import usage
17
18
19class Options(usage.Options, strcred.AuthOptionMixin):
20    """
21    An options list parser for twistd mail.
22
23    @type synopsis: L{bytes}
24    @ivar synopsis: A description of options for use in the usage message.
25
26    @type optParameters: L{list} of L{list} of (0) L{bytes}, (1) L{bytes},
27        (2) L{object}, (3) L{bytes}, (4) L{None} or
28        callable which takes L{bytes} and returns L{object}
29    @ivar optParameters: Information about supported parameters.  See
30        L{Options <twisted.python.usage.Options>} for details.
31
32    @type optFlags: L{list} of L{list} of (0) L{bytes}, (1) L{bytes} or
33        L{None}, (2) L{bytes}
34    @ivar optFlags: Information about supported flags.  See
35        L{Options <twisted.python.usage.Options>} for details.
36
37    @type _protoDefaults: L{dict} mapping L{bytes} to L{int}
38    @ivar _protoDefaults: A mapping of default service to port.
39
40    @type compData: L{Completions <usage.Completions>}
41    @ivar compData: Metadata for the shell tab completion system.
42
43    @type longdesc: L{bytes}
44    @ivar longdesc: A long description of the plugin for use in the usage
45        message.
46
47    @type service: L{MailService}
48    @ivar service: The email service.
49
50    @type last_domain: L{IDomain} provider or L{None}
51    @ivar last_domain: The most recently specified domain.
52    """
53
54    synopsis = "[options]"
55
56    optParameters = [
57        [
58            "relay",
59            "R",
60            None,
61            "Relay messages according to their envelope 'To', using "
62            "the given path as a queue directory.",
63        ],
64        ["hostname", "H", None, "The hostname by which to identify this server."],
65    ]
66
67    optFlags = [
68        ["esmtp", "E", "Use RFC 1425/1869 SMTP extensions"],
69        ["disable-anonymous", None, "Disallow non-authenticated SMTP connections"],
70        ["no-pop3", None, "Disable the default POP3 server."],
71        ["no-smtp", None, "Disable the default SMTP server."],
72    ]
73
74    _protoDefaults = {
75        "pop3": 8110,
76        "smtp": 8025,
77    }
78
79    compData = usage.Completions(optActions={"hostname": usage.CompleteHostnames()})
80
81    longdesc = """
82    An SMTP / POP3 email server plugin for twistd.
83
84    Examples:
85
86    1. SMTP and POP server
87
88    twistd mail --maildirdbmdomain=example.com=/tmp/example.com
89    --user=joe=password
90
91    Starts an SMTP server that only accepts emails to joe@example.com and saves
92    them to /tmp/example.com.
93
94    Also starts a POP mail server which will allow a client to log in using
95    username: joe@example.com and password: password and collect any email that
96    has been saved in /tmp/example.com.
97
98    2. SMTP relay
99
100    twistd mail --relay=/tmp/mail_queue
101
102    Starts an SMTP server that accepts emails to any email address and relays
103    them to an appropriate remote SMTP server. Queued emails will be
104    temporarily stored in /tmp/mail_queue.
105    """
106
107    def __init__(self):
108        """
109        Parse options and create a mail service.
110        """
111        usage.Options.__init__(self)
112        self.service = mail.MailService()
113        self.last_domain = None
114        for service in self._protoDefaults:
115            self[service] = []
116
117    def addEndpoint(self, service, description):
118        """
119        Add an endpoint to a service.
120
121        @type service: L{bytes}
122        @param service: A service, either C{b'smtp'} or C{b'pop3'}.
123
124        @type description: L{bytes}
125        @param description: An endpoint description string or a TCP port
126            number.
127        """
128        from twisted.internet import reactor
129
130        self[service].append(endpoints.serverFromString(reactor, description))
131
132    def opt_pop3(self, description):
133        """
134        Add a POP3 port listener on the specified endpoint.
135
136        You can listen on multiple ports by specifying multiple --pop3 options.
137        """
138        self.addEndpoint("pop3", description)
139
140    opt_p = opt_pop3
141
142    def opt_smtp(self, description):
143        """
144        Add an SMTP port listener on the specified endpoint.
145
146        You can listen on multiple ports by specifying multiple --smtp options.
147        """
148        self.addEndpoint("smtp", description)
149
150    opt_s = opt_smtp
151
152    def opt_default(self):
153        """
154        Make the most recently specified domain the default domain.
155        """
156        if self.last_domain:
157            self.service.addDomain("", self.last_domain)
158        else:
159            raise usage.UsageError("Specify a domain before specifying using --default")
160
161    opt_D = opt_default
162
163    def opt_maildirdbmdomain(self, domain):
164        """
165        Generate an SMTP/POP3 virtual domain.
166
167        This option requires an argument of the form 'NAME=PATH' where NAME is
168        the DNS domain name for which email will be accepted and where PATH is
169        a the filesystem path to a Maildir folder.
170        [Example: 'example.com=/tmp/example.com']
171        """
172        try:
173            name, path = domain.split("=")
174        except ValueError:
175            raise usage.UsageError(
176                "Argument to --maildirdbmdomain must be of the form 'name=path'"
177            )
178
179        self.last_domain = maildir.MaildirDirdbmDomain(
180            self.service, os.path.abspath(path)
181        )
182        self.service.addDomain(name, self.last_domain)
183
184    opt_d = opt_maildirdbmdomain
185
186    def opt_user(self, user_pass):
187        """
188        Add a user and password to the last specified domain.
189        """
190        try:
191            user, password = user_pass.split("=", 1)
192        except ValueError:
193            raise usage.UsageError(
194                "Argument to --user must be of the form 'user=password'"
195            )
196        if self.last_domain:
197            self.last_domain.addUser(user, password)
198        else:
199            raise usage.UsageError("Specify a domain before specifying users")
200
201    opt_u = opt_user
202
203    def opt_bounce_to_postmaster(self):
204        """
205        Send undeliverable messages to the postmaster.
206        """
207        self.last_domain.postmaster = 1
208
209    opt_b = opt_bounce_to_postmaster
210
211    def opt_aliases(self, filename):
212        """
213        Specify an aliases(5) file to use for the last specified domain.
214        """
215        if self.last_domain is not None:
216            if mail.IAliasableDomain.providedBy(self.last_domain):
217                aliases = alias.loadAliasFile(self.service.domains, filename)
218                self.last_domain.setAliasGroup(aliases)
219                self.service.monitor.monitorFile(
220                    filename, AliasUpdater(self.service.domains, self.last_domain)
221                )
222            else:
223                raise usage.UsageError(
224                    "%s does not support alias files"
225                    % (self.last_domain.__class__.__name__,)
226                )
227        else:
228            raise usage.UsageError("Specify a domain before specifying aliases")
229
230    opt_A = opt_aliases
231
232    def _getEndpoints(self, reactor, service):
233        """
234        Return a list of endpoints for the specified service, constructing
235        defaults if necessary.
236
237        If no endpoints were configured for the service and the protocol
238        was not explicitly disabled with a I{--no-*} option, a default
239        endpoint for the service is created.
240
241        @type reactor: L{IReactorTCP <twisted.internet.interfaces.IReactorTCP>}
242            provider
243        @param reactor: If any endpoints are created, the reactor with
244            which they are created.
245
246        @type service: L{bytes}
247        @param service: The type of service for which to retrieve endpoints,
248            either C{b'pop3'} or C{b'smtp'}.
249
250        @rtype: L{list} of L{IStreamServerEndpoint
251            <twisted.internet.interfaces.IStreamServerEndpoint>} provider
252        @return: The endpoints for the specified service as configured by the
253            command line parameters.
254        """
255        if self[service]:
256            # If there are any services set up, just return those.
257            return self[service]
258        elif self["no-" + service]:
259            # If there are no services, but the service was explicitly disabled,
260            # return nothing.
261            return []
262        else:
263            # Otherwise, return the old default service.
264            return [endpoints.TCP4ServerEndpoint(reactor, self._protoDefaults[service])]
265
266    def postOptions(self):
267        """
268        Check the validity of the specified set of options and
269        configure authentication.
270
271        @raise UsageError: When the set of options is invalid.
272        """
273        from twisted.internet import reactor
274
275        if self["esmtp"] and self["hostname"] is None:
276            raise usage.UsageError("--esmtp requires --hostname")
277
278        # If the --auth option was passed, this will be present -- otherwise,
279        # it won't be, which is also a perfectly valid state.
280        if "credCheckers" in self:
281            for ch in self["credCheckers"]:
282                self.service.smtpPortal.registerChecker(ch)
283
284        if not self["disable-anonymous"]:
285            self.service.smtpPortal.registerChecker(checkers.AllowAnonymousAccess())
286
287        anything = False
288        for service in self._protoDefaults:
289            self[service] = self._getEndpoints(reactor, service)
290            if self[service]:
291                anything = True
292
293        if not anything:
294            raise usage.UsageError("You cannot disable all protocols")
295
296
297class AliasUpdater:
298    """
299    A callable object which updates the aliases for a domain from an aliases(5)
300    file.
301
302    @ivar domains: See L{__init__}.
303    @ivar domain: See L{__init__}.
304    """
305
306    def __init__(self, domains, domain):
307        """
308        @type domains: L{dict} mapping L{bytes} to L{IDomain} provider
309        @param domains: A mapping of domain name to domain object
310
311        @type domain: L{IAliasableDomain} provider
312        @param domain: The domain to update.
313        """
314        self.domains = domains
315        self.domain = domain
316
317    def __call__(self, new):
318        """
319        Update the aliases for a domain from an aliases(5) file.
320
321        @type new: L{bytes}
322        @param new: The name of an aliases(5) file.
323        """
324        self.domain.setAliasGroup(alias.loadAliasFile(self.domains, new))
325
326
327def makeService(config):
328    """
329    Configure a service for operating a mail server.
330
331    The returned service may include POP3 servers, SMTP servers, or both,
332    depending on the configuration passed in.  If there are multiple servers,
333    they will share all of their non-network state (i.e. the same user accounts
334    are available on all of them).
335
336    @type config: L{Options <usage.Options>}
337    @param config: Configuration options specifying which servers to include in
338        the returned service and where they should keep mail data.
339
340    @rtype: L{IService <twisted.application.service.IService>} provider
341    @return: A service which contains the requested mail servers.
342    """
343    if config["esmtp"]:
344        rmType = relaymanager.SmartHostESMTPRelayingManager
345        smtpFactory = config.service.getESMTPFactory
346    else:
347        rmType = relaymanager.SmartHostSMTPRelayingManager
348        smtpFactory = config.service.getSMTPFactory
349
350    if config["relay"]:
351        dir = config["relay"]
352        if not os.path.isdir(dir):
353            os.mkdir(dir)
354
355        config.service.setQueue(relaymanager.Queue(dir))
356        default = relay.DomainQueuer(config.service)
357
358        manager = rmType(config.service.queue)
359        if config["esmtp"]:
360            manager.fArgs += (None, None)
361        manager.fArgs += (config["hostname"],)
362
363        helper = relaymanager.RelayStateHelper(manager, 1)
364        helper.setServiceParent(config.service)
365        config.service.domains.setDefaultDomain(default)
366
367    if config["pop3"]:
368        f = config.service.getPOP3Factory()
369        for endpoint in config["pop3"]:
370            svc = internet.StreamServerEndpointService(endpoint, f)
371            svc.setServiceParent(config.service)
372
373    if config["smtp"]:
374        f = smtpFactory()
375        if config["hostname"]:
376            f.domain = config["hostname"]
377            f.fArgs = (f.domain,)
378        if config["esmtp"]:
379            f.fArgs = (None, None) + f.fArgs
380        for endpoint in config["smtp"]:
381            svc = internet.StreamServerEndpointService(endpoint, f)
382            svc.setServiceParent(config.service)
383
384    return config.service
385