pythonmultithreadingsmtpasyncore

How to handle asyncore within a class in python, without blocking anything?


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.


Solution

  • 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 all asyncore channels are closed, as user Wessie pointed out in a comment before. Referring to the smtp example mentioned earlier, it turns out that smtpd.SMTPServer inherits from asyncore.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:

    class CustomSMTPServer(smtpd.SMTPServer):
        # store the emails in any form inside the custom SMTP server
        emails = []
        # overwrite the method that is used to process the received 
        # emails, putting them into self.emails for example
        def process_message(self, peer, mailfrom, rcpttos, data):
            # email processing
    
    
    class MyReceiver(object):
        def start(self):
            """Start the listening service"""
            # here I create an instance of the SMTP server, derived from  asyncore.dispatcher
            self.smtp = CustomSMTPServer(('0.0.0.0', 25), None)
            # and here I also start the asyncore loop, listening for SMTP connection, within a thread
            # timeout parameter is important, otherwise code will block 30 seconds after the smtp channel has been closed
            self.thread =  threading.Thread(target=asyncore.loop,kwargs = {'timeout':1} )
            self.thread.start()     
    
        def stop(self):
            """Stop listening now to port 25"""
            # close the SMTPserver to ensure no channels connect to asyncore
            self.smtp.close()
            # now it is save to wait for the thread to finish, i.e. for asyncore.loop() to exit
            self.thread.join()
    
        # now it finally it is possible to use an instance of this class to check for emails or whatever in a non-blocking way
        def count(self):
            """Return the number of emails received"""
            return len(self.smtp.emails)        
        def get(self):
            """Return all emails received so far"""
            return self.smtp.emails
        ....
    

    So in the end, I have a start and a stop method to start and stop listening on port 25 within a non-blocking environment.