aio-libs / aiosmtpd

A reimplementation of the Python stdlib smtpd.py based on asyncio.
https://aiosmtpd.aio-libs.org
Apache License 2.0
319 stars 96 forks source link

Missing multiple RFC support (MAIL FROM + RCPT TO) #367

Open perexg opened 1 year ago

perexg commented 1 year ago

I'm using this diff in my local smtp server which works as a proxy for the postfix. The aiosmtpd does not understand some parameters for MAIL FROM and RCPT TO:

diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index a977f75..300bfee 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -1281,6 +1281,9 @@ class SMTP(asyncio.StreamReaderProtocol):
                     '552 Error: message size exceeds fixed maximum message '
                     'size')
                 return
+        envid = params.pop('ENVID', None)  # RFC 3885
+        auth = params.pop('AUTH', None) # RFC 4954
+        ret = params.pop('RET', None)  # RFC 3461
         if len(params) > 0:
             await self.push(
                 '555 MAIL FROM parameters not recognized or not implemented')
@@ -1322,6 +1325,8 @@ class SMTP(asyncio.StreamReaderProtocol):
         if params is None:
             return await self.push(syntaxerr)
         # XXX currently there are no options we recognize.
+        orcpt = params.pop("ORCPT", None) # RFC 1891
+        notify = params.pop("NOTIFY", None) # RFC 3461
         if len(params) > 0:
             return await self.push(
                 '555 RCPT TO parameters not recognized or not implemented'

It would be nice, if someone can add those extensions. Also, it may be nice to have a list of parameters to be skipped in the SMTP class, so users can add the extra parameters to the class (using own class or assignments) without the need to modify directly the base class.

EDIT: v2 - added NOTIFY parameter

nim-odoo commented 1 year ago

If I understand correctly https://www.rfc-editor.org/rfc/rfc4954#section-5

For this reason, servers that advertise support for this extension MUST support the AUTH parameter to the MAIL FROM command even when the client has not authenticated itself to the server.

Since aiosmtpd advertises AUTH LOGIN PLAIN by default, it should not reject mails using AUTH in their MAIL FROM.

nim-odoo commented 1 year ago

Still early, but it seems the following fixes the issue related to AUTH=<> (which is the most common):

    async def handle_EHLO(self, server, session, envelope, hostname, response):
        """
        EHLO handler intended to remove the '250-AUTH' line from the response since we do not use
        any authentication.
        """

        session.host_name = hostname
        return [r for r in response if not r.startswith("250-AUTH")]

But of course it only works if clients properly implement the RFC.

perexg commented 1 year ago

I though about something like this (to allow override the SMTP class methods):

diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index a977f75..41bdf70 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -1225,6 +1225,10 @@ class SMTP(asyncio.StreamReaderProtocol):
         else:
             await self.push('501 Syntax: VRFY <address>')

+    def mail_cmd_params(self, params):
+        """Handle extra parameters for MAIL smtp command"""
+        return
+
     @syntax('MAIL FROM: <address>', extended=' [SP <mail-parameters>]')
     async def smtp_MAIL(self, arg: str) -> None:
         if await self.check_helo_needed():
@@ -1281,7 +1285,8 @@ class SMTP(asyncio.StreamReaderProtocol):
                     '552 Error: message size exceeds fixed maximum message '
                     'size')
                 return
-        if len(params) > 0:
+        r = self.mail_cmd_params(params)
+        if r or len(params) > 0:
             await self.push(
                 '555 MAIL FROM parameters not recognized or not implemented')
             return
@@ -1293,6 +1298,10 @@ class SMTP(asyncio.StreamReaderProtocol):
         log.info('%r sender: %s', self.session.peer, address)
         await self.push(status)

+    def rcpt_cmd_params(self, params):
+        """Handle extra parameters for RCPT smtp command"""
+        return
+
     @syntax('RCPT TO: <address>', extended=' [SP <mail-parameters>]')
     async def smtp_RCPT(self, arg: str) -> None:
         if await self.check_helo_needed():
@@ -1321,8 +1330,8 @@ class SMTP(asyncio.StreamReaderProtocol):
         params = self._getparams(rcpt_options)
         if params is None:
             return await self.push(syntaxerr)
-        # XXX currently there are no options we recognize.
-        if len(params) > 0:
+        r = self.rcpt_cmd_params(params)
+        if r or len(params) > 0:
             return await self.push(
                 '555 RCPT TO parameters not recognized or not implemented'
             )