smtp.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # Copyright (C) 2011 Sebastian Rahlf <basti at redtoad dot de>
  2. # with some ideas from http://code.activestate.com/recipes/440690/
  3. # SmtpMailsink Copyright 2005 Aviarc Corporation
  4. # Written by Adam Feuer, Matt Branthwaite, and Troy Frever
  5. # which is Licensed under the PSF License
  6. import email
  7. import aiosmtpd.controller
  8. class MessageDetails:
  9. def __init__(self, peer, mailfrom, rcpttos, *, mail_options=None, rcpt_options=None):
  10. self.peer = peer
  11. self.mailfrom = mailfrom
  12. self.rcpttos = rcpttos
  13. if mail_options:
  14. self.mail_options = mail_options
  15. if rcpt_options:
  16. self.rcpt_options = rcpt_options
  17. class Handler:
  18. def __init__(self):
  19. self.outbox = []
  20. async def handle_DATA(self, server, session, envelope):
  21. message = email.message_from_bytes(envelope.content)
  22. message.details = MessageDetails(session.peer, envelope.mail_from, envelope.rcpt_tos)
  23. self.outbox.append(message)
  24. return "250 OK"
  25. class Server(aiosmtpd.controller.Controller):
  26. """
  27. Small SMTP test server.
  28. This is little more than a wrapper around aiosmtpd.controller.Controller
  29. which offers a slightly different interface for backward compatibility with
  30. earlier versions of pytest-localserver. You can just as well use a standard
  31. Controller and pass it a Handler instance.
  32. Here is how to use this class for sending an email, if you really need to::
  33. server = Server(port=8080)
  34. server.start()
  35. print 'SMTP server is running on %s:%i' % server.addr
  36. # any e-mail sent to localhost:8080 will end up in server.outbox
  37. # ...
  38. server.stop()
  39. """
  40. def __init__(self, host="localhost", port=0):
  41. try:
  42. super().__init__(Handler(), hostname=host, port=port, server_hostname=host)
  43. except TypeError:
  44. # for aiosmtpd <1.3
  45. super().__init__(Handler(), hostname=host, port=port)
  46. @property
  47. def outbox(self):
  48. return self.handler.outbox
  49. def _set_server_socket_attributes(self):
  50. """
  51. Set the addr and port attributes on this Server instance, if they're not
  52. already set.
  53. """
  54. # I split this out into its own method to allow running this code in
  55. # aiosmtpd <1.4, which doesn't have the _trigger_server() method on
  56. # the Controller class. If I put it directly in _trigger_server(), it
  57. # would fail when calling super()._trigger_server(). In the future, when
  58. # we can safely require aiosmtpd >=1.4, this method can be inlined
  59. # directly into _trigger_server().
  60. if hasattr(self, "addr"):
  61. assert hasattr(self, "port")
  62. return
  63. self.addr = self.server.sockets[0].getsockname()[:2]
  64. # Work around a bug/missing feature in aiosmtpd (https://github.com/aio-libs/aiosmtpd/issues/276)
  65. if self.port == 0:
  66. self.port = self.addr[1]
  67. assert self.port != 0
  68. def _trigger_server(self):
  69. self._set_server_socket_attributes()
  70. super()._trigger_server()
  71. def is_alive(self):
  72. return self._thread is not None and self._thread.is_alive()
  73. @property
  74. def accepting(self):
  75. return self.server.is_serving()
  76. # for aiosmtpd <1.4
  77. if not hasattr(aiosmtpd.controller.Controller, "_trigger_server"):
  78. def start(self):
  79. super().start()
  80. self._set_server_socket_attributes()
  81. def stop(self, timeout=None):
  82. """
  83. Stops test server.
  84. :param timeout: When the timeout argument is present and not None, it
  85. should be a floating point number specifying a timeout for the
  86. operation in seconds (or fractions thereof).
  87. """
  88. # This mostly copies the implementation from Controller.stop(), with two
  89. # differences:
  90. # - It removes the assertion that the thread exists, allowing stop() to
  91. # be called more than once safely
  92. # - It passes the timeout argument to Thread.join()
  93. if self.loop.is_running():
  94. try:
  95. self.loop.call_soon_threadsafe(self.cancel_tasks)
  96. except AttributeError:
  97. # for aiosmtpd < 1.4.3
  98. self.loop.call_soon_threadsafe(self._stop)
  99. if self._thread is not None:
  100. self._thread.join(timeout)
  101. self._thread = None
  102. self._thread_exception = None
  103. self._factory_invoked = None
  104. self.server_coro = None
  105. self.server = None
  106. self.smtpd = None
  107. def __del__(self):
  108. # This is just for backward compatibility, to preserve the behavior that
  109. # the server is stopped when this object is finalized. But it seems
  110. # sketchy to rely on this to stop the server. Typically, the server
  111. # should be stopped "manually", before it gets deleted.
  112. if self.is_alive():
  113. self.stop()
  114. def __repr__(self): # pragma: no cover
  115. return "<smtp.Server %s:%s>" % self.addr
  116. def main():
  117. import time
  118. server = Server()
  119. server.start()
  120. print("SMTP server is running on %s:%i" % server.addr)
  121. print("Type <Ctrl-C> to stop")
  122. try:
  123. while True:
  124. time.sleep(1)
  125. except KeyboardInterrupt:
  126. pass
  127. finally:
  128. print("\rstopping...")
  129. server.stop()
  130. if __name__ == "__main__": # pragma: no cover
  131. main()