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