simple SMTP & POP3 servers

Implementation of a Python POP3 Server for Email Management

This paper presents the detailed implementation of a POP3 (Post Office Protocol Version 3) server in Python for email management. The server enables users to retrieve and manage their email messages. We discuss the motivation behind the project, in-depth implementation details, handling of POP3 commands, and testing results. The actual code for the server is referenced throughout the paper. Introduction

Email communication is an integral part of modern life, and protocols like POP3 play a vital role in email retrieval. The motivation behind this project was to create a POP3 server from scratch to gain a deeper understanding of email protocols and server development.

The primary objectives of this project include implementing a fully functional POP3 server in Python, handling key POP3 commands, managing email messages, ensuring robust error handling, and providing documentation for reference.

Background

POP3 Protocol

The POP3 protocol (defined in RFC 1939) is an industry-standard for retrieving email messages from a mail server. It operates over TCP and provides a set of commands for authentication, listing messages, retrieving messages, marking messages for deletion, and more.

Role of a POP3 Server

A POP3 server serves as the mail retrieval component in the email communication process. It allows email clients to connect, authenticate, and retrieve messages from the user’s mailbox.

Implementation Details

Server Architecture

The server is implemented in Python using the socket library for handling client connections. Multithreading is employed to handle multiple clients concurrently. A ThreadVariables` class is used to manage client-specific data and maintain state information.

@dataclass(frozen=True)
class Email():
    '''
    dataclass used to represent emails. (is used as a type.)
    number is the email id
    size is the size of the mail in bytes (= number of characters since utf8 char = 1 byte)
    '''
    number: int
    sender: str
    to: str
    subject: str
    received: str
    content: str
    size: int

    def to_str(self):
        '''
        Used to convert a mail to str to send it to the client.
        '''
        return f"From: {self.sender}\nTo: {self.to}\nSubject: {self.subject}\nReceived: {self.received}\n{self.content}"


@dataclass
class ThreadVariables():
    '''
    All variables for 1 thread, its main use is keeping arguments for functions clean and concise and allow easy access
    to the variables.
    global_lock: the lock used to lock a user's mailbox.
    A user's mailbox is locked by adding their username to the locked_files list. This list and global_lock are shared across all threads.
    username: the current client's username
    msg: the most recent message from the client. Includes command (and possible arguments)
    last_msg: the previous message sent by the client. This is only used in AUTHORIZATION state since a PASS command may only come after a valid USER command etc.
    mails: a list of Emails, all emails for a user are read once at the start of the TRANSACTION state and then stored in memory. 
    deleted_mails: a list of integers reffering to the deleted Emails' id's
    '''
    client: so.socket
    global_lock: threading.Lock
    # This list will be shared by all threads.
    locked_files: list[str]
    state: str = 'AUTHORIZATION'
    username: str = ''
    msg: str = ''
    last_msg: str = ''
    # field(default_factory=list) instantiates an empty (unique) list for each thread.
    mails: list[Email] = field(default_factory=list)
    deleted_mails: list[int] = field(default_factory=list)

Email Message Handling

Email messages are represented using the Email data class, storing attributes such as sender, recipient, subject, content, and size. The read_mails function reads email messages from the user’s mailbox, and the update_maildrop function updates the mailbox when a client quits.

Email message handling

def client_handler(client: so.socket, address: tuple[str, str], global_lock: threading.Lock, file_locks: list[str]):
    '''
    The function that each thread calls on creation. It handles one client connection.
    '''
    # ip and port of connected client.
    ip, port = address
    # client context manager (closes connection if an error would occur and if the context manager is exited.)
    with client:
        thread_variables: ThreadVariables = ThreadVariables(
            client, global_lock, file_locks)
        # set timeout of 10 minutes, if the server does not respond anything for 10 minutes, it will close the connection. (10min. was minimum wait time according to rfc)
        client.settimeout(600)
        # Server welcome message
        client.sendall("+OK POP3 server ready".encode())
        print(f"Client connected from {ip} at port {port}.")
        try:
            while True:
                # recieve client command.
                msg = client.recv(1024).decode()
                # empty message, most likely due to client that closed the connection.
                if not msg:
                    break
                print(f"The client at {ip}:{port} said: '{msg}'")
                # get the command issued by the client.
                match msg.split(" ")[0]:
                    case 'USER':
                        # update thread variables to have current message.
                        thread_variables.msg = msg
                        # handle command
                        USER(thread_variables)
                    case 'PASS':
                        thread_variables.msg = msg
                        PASS(thread_variables)
                    case 'STAT':
                        thread_variables.msg = msg
                        STAT(thread_variables)
                    case 'LIST':
                        thread_variables.msg = msg
                        LIST(thread_variables)
                    case 'RETR':
                        thread_variables.msg = msg
                        RETR(thread_variables)
                    case 'DELE':
                        thread_variables.msg = msg
                        DELE(thread_variables)
                    case 'RSET':
                        thread_variables.msg = msg
                        RSET(thread_variables)
                    case 'QUIT':
                        thread_variables.msg = msg
                        QUIT(thread_variables)
                        break
                    case _:
                        # default case
                        answer = "-ERR unrecognized command."
                        client.sendall(answer.encode())
        except so.timeout:
            print("Client timed out")
        except Exception as e:
            print(f"Error: {e}")
        finally:
            print(
                f"The client from ip: {ip}, and port: {port}, has diconnected!")
            # unlock maildrop (should be unlocked in QUIT(), but if the client pressed CTRL+C instead of quitting first, we will arrive here.)
            thread_variables.global_lock.acquire()
            try:
                thread_variables.locked_files.remove(thread_variables.username)
            # If the maildrop is already unlocked (username already removed from thread_variables.locked_files), a ValueError will be thrown, in this case: do nothing.
            except ValueError:
                pass
            thread_variables.global_lock.release()

Third-Party Libraries

No third-party libraries or modules are used in this project. The server is implemented using core Python functionality, ensuring simplicity and portability.

Error Handling

The server includes robust error-handling mechanisms to ensure graceful handling of client interactions, socket errors, and unexpected behavior. Errors are logged, and appropriate error messages are sent to clients.

POP3 Commands

USER, PASS, STAT, LIST, RETR, DELE, RSET, QUIT

Each POP3 command is implemented as a separate function, adhering to the POP3 protocol’s specifications. These functions manage client authentication, message retrieval, and mailbox management.

Helper Functions

Several helper functions assist in handling email messages, user authentication, and mailbox operations. These functions contribute to the server’s overall functionality.

Testing

Testing was conducted by simulating client-server interactions using various email clients. The server was tested for correctness, performance, and reliability. Challenges during testing were resolved, ensuring the server’s robustness.

# Testing procedures

Results

The POP3 server demonstrated reliable performance during testing, handling multiple clients concurrently. Resource usage remained within acceptable limits, and response times were reasonable. The server successfully managed email messages and provided the expected functionality.

Conclusion

The implementation of a Python POP3 server for email management has achieved its objectives. It provides a functional server that adheres to the POP3 protocol, allowing users to retrieve and manage email messages. Challenges encountered during development and testing were overcome, resulting in a robust server implementation. Future Work

Future enhancements could include implementing additional POP3 extensions, improving error handling for edge cases, optimizing server performance further, and enhancing security features, such as encryption and authentication mechanisms. Acknowledgments

We acknowledge the guidance and support received from [Mentor Name] during the course of this project.

References

  • RFC 1939: “Post Office Protocol - Version 3”
  • RFC 821: “SIMPLE MAIL TRANSFER PROTOCOL”

Authors: Ibrahim El Kaddouri, Simon Desimpelaere