I need to create a class that can receive and store SMTP messages, i.e. E-Mails. To do so, I am using asyncore
according to an example posted here. However, asyncore.loop()
is blocking so I cannot do anything else in the code.
So I thought of using threads. Here is an example-code that shows what I have in mind:
class MyServer(smtpd.SMTPServer):
# derive from the python server class
def process_message(..):
# overwrite a smtpd.SMTPServer method to be able to handle the received messages
...
self.list_emails.append(this_email)
def get_number_received_emails(self):
"""Return the current number of stored emails"""
return len(self.list_emails)
def start_receiving(self):
"""Start the actual server to listen on port 25"""
self.thread = threading.Thread(target=asyncore.loop)
self.thread.start()
def stop(self):
"""Stop listening now to port 25"""
# close the SMTPserver from itself
self.close()
self.thread.join()
I hope you get the picture. The class MyServer
should be able to start and stop listening to port 25 in a non-blocking way, able to be queried for messages while listening (or not). The start
method starts the asyncore.loop()
listener, which, when a reception of an email occurs, append to an internal list. Similar, the stop
method should be able to stop this server, as suggested here.
Despite the fact this code does not work as I expect to (asyncore seems to run forever, even I call the above stop
method. The error
I raise is catched within stop
, but not within the target
function containing asyncore.loop()
), I am not sure if my approach to the problem is senseful. Any suggestions for fixing the above code or proposing a more solid implementation (without using third party software), are appreciated.
In case anyone else needs this fleshed out a bit more, here's what I ended up using. This uses smtpd for the email server and smtpblib for the email client, with Flask as a http server [gist]:
app.py
smtp_server.py
smtp_client.py
Then start the server with
python app.py
and in another request simulate a request to/send_email
withcurl localhost:5000/send_email
. Note that to actually send the email (or sms) you'll need to jump through other hoops detailed here: https://blog.codinghorror.com/so-youd-like-to-send-some-email-through-code/.Coming from the other question asyncore.loop doesn't terminate when there are no more connections
I think you are slightly over thinking the threading. Using the code from the other question, you can start a new thread that runs the
asyncore.loop
by the following code snippet:This will run it in a new thread and will keep going till all
asyncore
channels are closed.The solution provided might not be the most sophisticated solution, but it works reasonable and has been tested.
First of all, the matter with
asyncore.loop()
is that it blocks until allasyncore
channels are closed, as user Wessie pointed out in a comment before. Referring to the smtp example mentioned earlier, it turns out thatsmtpd.SMTPServer
inherits fromasyncore.dispatcher
(as described on the smtpd documentation), which answers the question of which channel to be closed.Therefore, the original question can be answered with the following updated example code:
So in the end, I have a
start
and astop
method to start and stop listening on port 25 within a non-blocking environment.You should consider using Twisted, instead. http://twistedmatrix.com/trac/browser/trunk/doc/mail/examples/emailserver.tac demonstrates how to set up an SMTP server with a customizable on-delivery hook.
Alex answer is the best but was incomplete for my use case. I wanted to test SMTP as part of a unit test which meant building the fake SMTP server inside my test objects and the server would not terminate the asyncio thread so I had to add a line to set it to a daemon thread to allow the rest of the unit test to complete without blocking waiting for that asyncio thread to join. I also added in complete logging of all email data so that I could assert anything sent through the SMTP.
Here is my fake SMTP class:
And here is how it is called by the unittest classes: