Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python aiosmtpd - what is missing for an Mail-Transfer-Agent (MTA)?

I want to write my own small mailserver application in python with aiosmtpd

a) for educational purpose to better understand mailservers
b) to realize my own features

So my question is, what is missing (besides aiosmtpd) for an Mail-Transfer-Agent, that can send and receive emails to/from other full MTAs (gmail.com, yahoo.com ...)?

I'm guessing:

1.) Of course a domain and static ip
2.) Valid certificate for this domain
...should be doable with Lets Encrypt
3.) Encryption
...should be doable with SSL/Context/Starttls... with aiosmtpd itself
4.) Resolving MX DNS entries for outgoing emails!?
...should be doable with python library dnspython
5.) Error handling for SMTP communication errors, error replies from other MTAs, bouncing!?
6.) Queue for handling inbound and pending outbund emails!?

Are there any other "essential" features missing?

Of course i know, there are a lot more "advanced" features for a mailserver like spam checking, malware checking, certificate validation, blacklisting, rules, mailboxes and more...

Thanks for all hints!


EDIT:

Let me clarify what is in my mind:
I want to write a mailserver for a club. Its main purpose will be a mailing-list-server. There will be different lists for different groups of the club. Lets say my domain is myclub.org then there will be for example [email protected], [email protected] and so on.
Only members will be allowed to use this mailserver and only the members will receive emails from this mailserver. No one else will be allowed to send emails to this mailserver nor will receive emails from it. The members email-addresses and their group(s) are stored in a database.

In the future i want to integrate some other useful features, for example:

  • Auto-reminders
  • A chatbot, where members can control services and request informations by email

What i don't need:

  • User Mailboxes
  • POP/IMAP access
  • Webinterface

Open relay issue:

  • I want to reject any [FROM] email address that is not in the members database during SMTP negotiation.
  • I want to check the sending mailservers for a valid certificate.
  • The number of emails/member/day will be limited.
  • I'm not sure, if i really need spam detection for the incoming emails?

Losing emails issue:

I think i will need a "lightweight" retry mechanism. However if an outgoing email can't be delivered after some retries, it will be dropped and only the administrator will be notified, not the sender. The members should not be bothered by email delivery issues. Is there any Python Library that can generate RFC3464 compliant error reply emails?

Reboot issue:

I'm not sure if i really need persistent storage for emails, that are not yet sent? In my use case, all the outgoing emails should be delivered usually within a few seconds (if no delivery problem occurs). Before a (planned) reboot i can check for an empty send queue.

like image 541
Franky1 Avatar asked Jan 03 '23 14:01

Franky1


1 Answers

aiosmtpd is an excellent tool for writing custom routing and header rewriting rules for email. However, aiosmtpd is not an MTA, since it does not do message queuing or DSN generation. One popular choice of MTA is postfix, and since postfix can be configured to relay all emails for a domain to another local SMTP server (such as aiosmtpd), a natural choice is to use postfix as the internet-facing frontend and aiosmtpd as the business-logic backend.

Advantages of using postfix as the middle-man instead of letting aiosmtpd face the public internet:

  • No need to handle DNS MX lookups in aiosmtpd -- just relay through postfix (localhost:25)
  • No worry about non-compliant SMTP clients in aiosmtpd
  • No worry about STARTTLS in aiosmtpd -- configure this in postfix instead (simpler and more battle-hardened)
  • No worry about retrying failed email deliveries and sending delivery status notifications
  • aiosmtpd can be configured to respond with "transient failure" (SMTP 4xx code) upon programming errors, so no email is lost as long as the programming error is fixed within 4 days

Here's how you might configure postfix to work with a local SMTP server powered by e.g. aiosmtpd.

We're going to run postfix on port 25 and aiosmtpd on port 20381.

To specify that postfix should relay emails for example.com to an SMTP server running on port 20381, add the following to /etc/postfix/main.cf:

transport_maps = hash:/etc/postfix/smtp_transport
relay_domains = example.com

And create /etc/postfix/smtp_transport with the contents:

# Table of special transport method for domains in
# virtual_mailbox_domains. See postmap(5), virtual(5) and
# transport(5).
#
# Remember to run
#     postmap /etc/postfix/smtp_transport
# and update relay_domains in main.cf after changing this file!
example.com   smtp:127.0.0.1:20381

Run postmap /etc/postfix/smtp_transport after creating that file (and every time you modify it).


On the aiosmtpd side, there are a few things to consider.

The most important is how you handle bounce emails. The short story is that you should set the envelope sender to an email address you control that is dedicated to receiving bounces, e.g. [email protected]. When email arrives at this address, it should be stored somewhere so you can process bounces, e.g. by removing member email addresses from your database.

Another important thing to consider is how you tell your members' email providers that you are doing mailing list forwarding. You might want to add the following headers when forwarding emails to [email protected]:

Sender: [email protected]
List-Name: GROUP
List-Id: GROUP.example.com
List-Unsubscribe: <mailto:[email protected]?subject=unsubscribe%20GROUP>
List-Help: <mailto:[email protected]?subject=list-help>
List-Subscribe: <mailto:[email protected]?subject=subscribe%20GROUP>
Precedence: bulk
X-Auto-Response-Suppress: OOF

Here, I used [email protected] as the recipient for list unsubscribe requests. This should be an address that forwards to the email administrator (that is, you).

Below is a skeleton (untested) that does the above. It stores bounce emails in a directory named bounces and forwards emails with a valid From:-header (one that appears in MEMBERS) according to the list of groups (in GROUPS).

import os
import email
import email.utils
import mailbox
import smtplib
import aiosmtpd.controller

LISTEN_HOST = '127.0.0.1'
LISTEN_PORT = 20381
DOMAIN = 'example.com'
BOUNCE_ADDRESS = 'bounce'
POSTMASTER = 'postmaster'
BOUNCE_DIRECTORY = os.path.join(
    os.path.dirname(__file__), 'bounces')


def get_extra_headers(list_name, is_group=True, skip=()):
    list_id = '%s.%s' % (list_name, DOMAIN)
    bounce = '%s@%s' % (BOUNCE_ADDRESS, DOMAIN)
    postmaster = '%s@%s' % (POSTMASTER, DOMAIN)
    unsub = '<mailto:%s?subject=unsubscribe%%20%s>' % (postmaster, list_name)
    help = '<mailto:%s?subject=list-help>' % (postmaster,)
    sub = '<mailto:%s?subject=subscribe%%20%s>' % (postmaster, list_name)
    headers = [
        ('Sender', bounce),
        ('List-Name', list_name),
        ('List-Id', list_id),
        ('List-Unsubscribe', unsub),
        ('List-Help', help),
        ('List-Subscribe', sub),
    ]
    if is_group:
        headers.extend([
            ('Precedence', 'bulk'),
            ('X-Auto-Response-Suppress', 'OOF'),
        ])
    headers = [(k, v) for k, v in headers if k.lower() not in skip]
    return headers


def store_bounce_message(message):
    mbox = mailbox.Maildir(BOUNCE_DIRECTORY)
    mbox.add(message)


MEMBERS = ['[email protected]', '[email protected]',
           '[email protected]']

GROUPS = {
    'group1': ['[email protected]', '[email protected]'],
    POSTMASTER: ['[email protected]'],
}


class ClubHandler:
    def validate_sender(self, message):
        from_ = message.get('From')
        if not from_:
            return False
        realname, address = email.utils.parseaddr(from_)
        if address not in MEMBERS:
            return False
        return True

    def translate_recipient(self, local_part):
        try:
            return GROUPS[local_part]
        except KeyError:
            return None

    async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
        local, domain = address.split('@')
        if domain.lower() != DOMAIN:
            return '550 wrong domain'
        if local.lower() == BOUNCE:
            envelope.is_bounce = True
            return '250 OK'
        translated = self.translate_recipient(local.lower())
        if translated is None:
            return '550 no such user'
        envelope.rcpt_tos.extend(translated)
        return '250 OK'

    async def handle_DATA(self, server, session, envelope):
        if getattr(envelope, 'is_bounce', False):
            if len(envelope.rcpt_tos) > 0:
                return '500 Cannot send bounce message to multiple recipients'
            store_bounce_message(envelope.original_content)
            return '250 OK'

        message = email.message_from_bytes(envelope.original_content)
        if not self.validate_sender(message):
            return '500 I do not know you'

        for header_key, header_value in get_extra_headers('club'):
            message[header_key] = header_value

        bounce = '%s@%s' % (BOUNCE_ADDRESS, DOMAIN)
        with smtplib.SMTP('localhost', 25) as smtp:
            smtp.sendmail(bounce, envelope.rcpt_tos, message.as_bytes())

        return '250 OK'


if __name__ == '__main__':
    controller = aiosmtpd.controller.Controller(ClubHandler, hostname=LISTEN_HOST, port=LISTEN_PORT)
    controller.start()
    print("Controller started")
    try:
        while True:
            input()
    except (EOFError, KeyboardInterrupt):
        controller.stop()
like image 167
Mathias Rav Avatar answered Jan 06 '23 06:01

Mathias Rav