123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- # -*- test-case-name: twisted.mail.test.test_options -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- Support for creating mail servers with twistd.
- """
- import os
- from twisted.mail import mail
- from twisted.mail import maildir
- from twisted.mail import relay
- from twisted.mail import relaymanager
- from twisted.mail import alias
- from twisted.internet import endpoints
- from twisted.python import usage
- from twisted.cred import checkers
- from twisted.cred import strcred
- from twisted.application import internet
- class Options(usage.Options, strcred.AuthOptionMixin):
- """
- An options list parser for twistd mail.
- @type synopsis: L{bytes}
- @ivar synopsis: A description of options for use in the usage message.
- @type optParameters: L{list} of L{list} of (0) L{bytes}, (1) L{bytes},
- (2) L{object}, (3) L{bytes}, (4) L{None} or
- callable which takes L{bytes} and returns L{object}
- @ivar optParameters: Information about supported parameters. See
- L{Options <twisted.python.usage.Options>} for details.
- @type optFlags: L{list} of L{list} of (0) L{bytes}, (1) L{bytes} or
- L{None}, (2) L{bytes}
- @ivar optFlags: Information about supported flags. See
- L{Options <twisted.python.usage.Options>} for details.
- @type _protoDefaults: L{dict} mapping L{bytes} to L{int}
- @ivar _protoDefaults: A mapping of default service to port.
- @type compData: L{Completions <usage.Completions>}
- @ivar compData: Metadata for the shell tab completion system.
- @type longdesc: L{bytes}
- @ivar longdesc: A long description of the plugin for use in the usage
- message.
- @type service: L{MailService}
- @ivar service: The email service.
- @type last_domain: L{IDomain} provider or L{None}
- @ivar last_domain: The most recently specified domain.
- """
- synopsis = "[options]"
- optParameters = [
- ["relay", "R", None,
- "Relay messages according to their envelope 'To', using "
- "the given path as a queue directory."],
- ["hostname", "H", None,
- "The hostname by which to identify this server."],
- ]
- optFlags = [
- ["esmtp", "E", "Use RFC 1425/1869 SMTP extensions"],
- ["disable-anonymous", None,
- "Disallow non-authenticated SMTP connections"],
- ["no-pop3", None, "Disable the default POP3 server."],
- ["no-smtp", None, "Disable the default SMTP server."],
- ]
- _protoDefaults = {
- "pop3": 8110,
- "smtp": 8025,
- }
- compData = usage.Completions(
- optActions={"hostname": usage.CompleteHostnames()}
- )
- longdesc = """
- An SMTP / POP3 email server plugin for twistd.
- Examples:
- 1. SMTP and POP server
- twistd mail --maildirdbmdomain=example.com=/tmp/example.com
- --user=joe=password
- Starts an SMTP server that only accepts emails to joe@example.com and saves
- them to /tmp/example.com.
- Also starts a POP mail server which will allow a client to log in using
- username: joe@example.com and password: password and collect any email that
- has been saved in /tmp/example.com.
- 2. SMTP relay
- twistd mail --relay=/tmp/mail_queue
- Starts an SMTP server that accepts emails to any email address and relays
- them to an appropriate remote SMTP server. Queued emails will be
- temporarily stored in /tmp/mail_queue.
- """
- def __init__(self):
- """
- Parse options and create a mail service.
- """
- usage.Options.__init__(self)
- self.service = mail.MailService()
- self.last_domain = None
- for service in self._protoDefaults:
- self[service] = []
- def addEndpoint(self, service, description):
- """
- Add an endpoint to a service.
- @type service: L{bytes}
- @param service: A service, either C{b'smtp'} or C{b'pop3'}.
- @type description: L{bytes}
- @param description: An endpoint description string or a TCP port
- number.
- """
- from twisted.internet import reactor
- self[service].append(endpoints.serverFromString(reactor, description))
- def opt_pop3(self, description):
- """
- Add a POP3 port listener on the specified endpoint.
- You can listen on multiple ports by specifying multiple --pop3 options.
- """
- self.addEndpoint('pop3', description)
- opt_p = opt_pop3
- def opt_smtp(self, description):
- """
- Add an SMTP port listener on the specified endpoint.
- You can listen on multiple ports by specifying multiple --smtp options.
- """
- self.addEndpoint('smtp', description)
- opt_s = opt_smtp
- def opt_default(self):
- """
- Make the most recently specified domain the default domain.
- """
- if self.last_domain:
- self.service.addDomain('', self.last_domain)
- else:
- raise usage.UsageError("Specify a domain before specifying using --default")
- opt_D = opt_default
- def opt_maildirdbmdomain(self, domain):
- """
- Generate an SMTP/POP3 virtual domain.
- This option requires an argument of the form 'NAME=PATH' where NAME is
- the DNS domain name for which email will be accepted and where PATH is
- a the filesystem path to a Maildir folder.
- [Example: 'example.com=/tmp/example.com']
- """
- try:
- name, path = domain.split('=')
- except ValueError:
- raise usage.UsageError("Argument to --maildirdbmdomain must be of the form 'name=path'")
- self.last_domain = maildir.MaildirDirdbmDomain(self.service, os.path.abspath(path))
- self.service.addDomain(name, self.last_domain)
- opt_d = opt_maildirdbmdomain
- def opt_user(self, user_pass):
- """
- Add a user and password to the last specified domain.
- """
- try:
- user, password = user_pass.split('=', 1)
- except ValueError:
- raise usage.UsageError("Argument to --user must be of the form 'user=password'")
- if self.last_domain:
- self.last_domain.addUser(user, password)
- else:
- raise usage.UsageError("Specify a domain before specifying users")
- opt_u = opt_user
- def opt_bounce_to_postmaster(self):
- """
- Send undeliverable messages to the postmaster.
- """
- self.last_domain.postmaster = 1
- opt_b = opt_bounce_to_postmaster
- def opt_aliases(self, filename):
- """
- Specify an aliases(5) file to use for the last specified domain.
- """
- if self.last_domain is not None:
- if mail.IAliasableDomain.providedBy(self.last_domain):
- aliases = alias.loadAliasFile(self.service.domains, filename)
- self.last_domain.setAliasGroup(aliases)
- self.service.monitor.monitorFile(
- filename,
- AliasUpdater(self.service.domains, self.last_domain)
- )
- else:
- raise usage.UsageError(
- "%s does not support alias files" % (
- self.last_domain.__class__.__name__,
- )
- )
- else:
- raise usage.UsageError("Specify a domain before specifying aliases")
- opt_A = opt_aliases
- def _getEndpoints(self, reactor, service):
- """
- Return a list of endpoints for the specified service, constructing
- defaults if necessary.
- If no endpoints were configured for the service and the protocol
- was not explicitly disabled with a I{--no-*} option, a default
- endpoint for the service is created.
- @type reactor: L{IReactorTCP <twisted.internet.interfaces.IReactorTCP>}
- provider
- @param reactor: If any endpoints are created, the reactor with
- which they are created.
- @type service: L{bytes}
- @param service: The type of service for which to retrieve endpoints,
- either C{b'pop3'} or C{b'smtp'}.
- @rtype: L{list} of L{IStreamServerEndpoint
- <twisted.internet.interfaces.IStreamServerEndpoint>} provider
- @return: The endpoints for the specified service as configured by the
- command line parameters.
- """
- if self[service]:
- # If there are any services set up, just return those.
- return self[service]
- elif self['no-' + service]:
- # If there are no services, but the service was explicitly disabled,
- # return nothing.
- return []
- else:
- # Otherwise, return the old default service.
- return [
- endpoints.TCP4ServerEndpoint(
- reactor, self._protoDefaults[service])]
- def postOptions(self):
- """
- Check the validity of the specified set of options and
- configure authentication.
- @raise UsageError: When the set of options is invalid.
- """
- from twisted.internet import reactor
- if self['esmtp'] and self['hostname'] is None:
- raise usage.UsageError("--esmtp requires --hostname")
- # If the --auth option was passed, this will be present -- otherwise,
- # it won't be, which is also a perfectly valid state.
- if 'credCheckers' in self:
- for ch in self['credCheckers']:
- self.service.smtpPortal.registerChecker(ch)
- if not self['disable-anonymous']:
- self.service.smtpPortal.registerChecker(checkers.AllowAnonymousAccess())
- anything = False
- for service in self._protoDefaults:
- self[service] = self._getEndpoints(reactor, service)
- if self[service]:
- anything = True
- if not anything:
- raise usage.UsageError("You cannot disable all protocols")
- class AliasUpdater:
- """
- A callable object which updates the aliases for a domain from an aliases(5)
- file.
- @ivar domains: See L{__init__}.
- @ivar domain: See L{__init__}.
- """
- def __init__(self, domains, domain):
- """
- @type domains: L{dict} mapping L{bytes} to L{IDomain} provider
- @param domains: A mapping of domain name to domain object
- @type domain: L{IAliasableDomain} provider
- @param domain: The domain to update.
- """
- self.domains = domains
- self.domain = domain
- def __call__(self, new):
- """
- Update the aliases for a domain from an aliases(5) file.
- @type new: L{bytes}
- @param new: The name of an aliases(5) file.
- """
- self.domain.setAliasGroup(alias.loadAliasFile(self.domains, new))
- def makeService(config):
- """
- Configure a service for operating a mail server.
- The returned service may include POP3 servers, SMTP servers, or both,
- depending on the configuration passed in. If there are multiple servers,
- they will share all of their non-network state (i.e. the same user accounts
- are available on all of them).
- @type config: L{Options <usage.Options>}
- @param config: Configuration options specifying which servers to include in
- the returned service and where they should keep mail data.
- @rtype: L{IService <twisted.application.service.IService>} provider
- @return: A service which contains the requested mail servers.
- """
- if config['esmtp']:
- rmType = relaymanager.SmartHostESMTPRelayingManager
- smtpFactory = config.service.getESMTPFactory
- else:
- rmType = relaymanager.SmartHostSMTPRelayingManager
- smtpFactory = config.service.getSMTPFactory
- if config['relay']:
- dir = config['relay']
- if not os.path.isdir(dir):
- os.mkdir(dir)
- config.service.setQueue(relaymanager.Queue(dir))
- default = relay.DomainQueuer(config.service)
- manager = rmType(config.service.queue)
- if config['esmtp']:
- manager.fArgs += (None, None)
- manager.fArgs += (config['hostname'],)
- helper = relaymanager.RelayStateHelper(manager, 1)
- helper.setServiceParent(config.service)
- config.service.domains.setDefaultDomain(default)
- if config['pop3']:
- f = config.service.getPOP3Factory()
- for endpoint in config['pop3']:
- svc = internet.StreamServerEndpointService(endpoint, f)
- svc.setServiceParent(config.service)
- if config['smtp']:
- f = smtpFactory()
- if config['hostname']:
- f.domain = config['hostname']
- f.fArgs = (f.domain,)
- if config['esmtp']:
- f.fArgs = (None, None) + f.fArgs
- for endpoint in config['smtp']:
- svc = internet.StreamServerEndpointService(endpoint, f)
- svc.setServiceParent(config.service)
- return config.service
|