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