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