7
7
# Written by Adam Feuer, Matt Branthwaite, and Troy Frever
8
8
# which is Licensed under the PSF License
9
9
10
- import asyncore
10
+ import aiosmtpd . controller
11
11
import email
12
- import smtpd
13
12
import sys
14
- import threading
15
13
16
14
17
- PY35_OR_NEWER = sys .version_info [:2 ] >= (3 , 5 )
15
+ class MessageDetails :
16
+ def __init__ (self , peer , mailfrom , rcpttos , * , mail_options = None , rcpt_options = None ):
17
+ self .peer = peer
18
+ self .mailfrom = mailfrom
19
+ self .rcpttos = rcpttos
20
+ if mail_options :
21
+ self .mail_options = mail_options
22
+ if rcpt_options :
23
+ self .rcpt_options = rcpt_options
18
24
19
- class Server (smtpd .SMTPServer , threading .Thread ):
25
+
26
+ class Handler :
27
+ def __init__ (self ):
28
+ self .outbox = []
29
+
30
+ async def handle_DATA (self , server , session , envelope ):
31
+ message = email .message_from_bytes (envelope .content )
32
+ message .details = MessageDetails (session .peer , envelope .mail_from , envelope .rcpt_tos )
33
+ self .outbox .append (message )
34
+ return '250 OK'
35
+
36
+
37
+ class Server (aiosmtpd .controller .Controller ):
20
38
21
39
"""
22
- Small SMTP test server. Try the following snippet for sending mail::
40
+ Small SMTP test server.
41
+
42
+ This is little more than a wrapper around aiosmtpd.controller.Controller
43
+ which offers a slightly different interface for backward compatibility with
44
+ earlier versions of pytest-localserver. You can just as well use a standard
45
+ Controller and pass it a Handler instance.
46
+
47
+ Here is how to use this class for sending an email, if you really need to::
23
48
24
49
server = Server(port=8080)
25
50
server.start()
@@ -32,68 +57,97 @@ class Server (smtpd.SMTPServer, threading.Thread):
32
57
33
58
"""
34
59
35
- WAIT_BETWEEN_CHECKS = 0.001
36
-
37
60
def __init__ (self , host = 'localhost' , port = 0 ):
38
- # Workaround for deprecated signature in Python 3.6
39
- if PY35_OR_NEWER :
40
- smtpd .SMTPServer .__init__ (self , (host , port ), None , decode_data = True )
41
- else :
42
- smtpd .SMTPServer .__init__ (self , (host , port ), None )
43
-
44
- if self ._localaddr [1 ] == 0 :
45
- self .addr = self .socket .getsockname ()
46
-
47
- self .outbox = []
61
+ try :
62
+ super ().__init__ (Handler (), hostname = host , port = port , server_hostname = host )
63
+ except TypeError :
64
+ # for aiosmtpd <1.3
65
+ super ().__init__ (Handler (), hostname = host , port = port )
48
66
49
- # initialise thread
50
- self ._stopevent = threading .Event ()
51
- self .threadName = self .__class__ .__name__
52
- threading .Thread .__init__ (self , name = self .threadName )
67
+ @property
68
+ def outbox (self ):
69
+ return self .handler .outbox
53
70
54
- def process_message (self , peer , mailfrom , rcpttos , data , ** kwargs ):
71
+ def _set_server_socket_attributes (self ):
55
72
"""
56
- Adds message to outbox.
73
+ Set the addr and port attributes on this Server instance, if they're not
74
+ already set.
57
75
"""
76
+
77
+ # I split this out into its own method to allow running this code in
78
+ # aiosmtpd <1.4, which doesn't have the _trigger_server() method on
79
+ # the Controller class. If I put it directly in _trigger_server(), it
80
+ # would fail when calling super()._trigger_server(). In the future, when
81
+ # we can safely require aiosmtpd >=1.4, this method can be inlined
82
+ # directly into _trigger_server().
83
+ if hasattr (self , 'addr' ):
84
+ assert hasattr (self , 'port' )
85
+ return
86
+
87
+ self .addr = self .server .sockets [0 ].getsockname ()[:2 ]
88
+
89
+ # Work around a bug/missing feature in aiosmtpd (https://github.com/aio-libs/aiosmtpd/issues/276)
90
+ if self .port == 0 :
91
+ self .port = self .addr [1 ]
92
+ assert self .port != 0
93
+
94
+ def _trigger_server (self ):
95
+ self ._set_server_socket_attributes ()
96
+ super ()._trigger_server ()
97
+
98
+ def is_alive (self ):
99
+ return self ._thread is not None and self ._thread .is_alive ()
100
+
101
+ @property
102
+ def accepting (self ):
58
103
try :
59
- message = email . message_from_bytes ( data )
104
+ return self . server . is_serving ( )
60
105
except AttributeError :
61
- message = email .message_from_string (data )
62
- # on the message, also set the envelope details
63
-
64
- class Bunch :
65
- def __init__ (self , ** kwds ):
66
- vars (self ).update (kwds )
67
-
68
- message .details = Bunch (
69
- peer = peer ,
70
- mailfrom = mailfrom ,
71
- rcpttos = rcpttos ,
72
- ** kwargs
73
- )
74
- self .outbox .append (message )
75
-
76
- def run (self ):
77
- """
78
- Threads run method.
79
- """
80
- while not self ._stopevent .is_set ():
81
- asyncore .loop (timeout = self .WAIT_BETWEEN_CHECKS , count = 1 )
106
+ # asyncio.base_events.Server.is_serving() only exists in Python 3.6
107
+ # and up. For Python 3.5, asyncio.base_events.BaseEventLoop.is_running()
108
+ # is a close approximation; it should mostly return the same value
109
+ # except for brief periods when the server is starting up or shutting
110
+ # down. Once we drop support for Python 3.5, this branch becomes
111
+ # unnecessary.
112
+ return self .loop .is_running ()
113
+
114
+ # for aiosmtpd <1.4
115
+ if not hasattr (aiosmtpd .controller .Controller , '_trigger_server' ):
116
+ def start (self ):
117
+ super ().start ()
118
+ self ._set_server_socket_attributes ()
82
119
83
120
def stop (self , timeout = None ):
84
121
"""
85
122
Stops test server.
86
-
87
123
:param timeout: When the timeout argument is present and not None, it
88
124
should be a floating point number specifying a timeout for the
89
125
operation in seconds (or fractions thereof).
90
126
"""
91
- self ._stopevent .set ()
92
- threading .Thread .join (self , timeout )
93
- self .close ()
127
+
128
+ # This mostly copies the implementation from Controller.stop(), with two
129
+ # differences:
130
+ # - It removes the assertion that the thread exists, allowing stop() to
131
+ # be called more than once safely
132
+ # - It passes the timeout argument to Thread.join()
133
+ if self .loop .is_running ():
134
+ self .loop .call_soon_threadsafe (self ._stop )
135
+ if self ._thread is not None :
136
+ self ._thread .join (timeout )
137
+ self ._thread = None
138
+ self ._thread_exception = None
139
+ self ._factory_invoked = None
140
+ self .server_coro = None
141
+ self .server = None
142
+ self .smtpd = None
94
143
95
144
def __del__ (self ):
96
- self .stop ()
145
+ # This is just for backward compatibility, to preserve the behavior that
146
+ # the server is stopped when this object is finalized. But it seems
147
+ # sketchy to rely on this to stop the server. Typically, the server
148
+ # should be stopped "manually", before it gets deleted.
149
+ if self .is_alive ():
150
+ self .stop ()
97
151
98
152
def __repr__ (self ): # pragma: no cover
99
153
return '<smtp.Server %s:%s>' % self .addr
0 commit comments