218 lines
9.0 KiB
Diff
218 lines
9.0 KiB
Diff
From b9387743835821e8327e73aa502cb01f2f83dc97 Mon Sep 17 00:00:00 2001
|
|
From: Patrick Cernko <errror+gitlab.com@errror.org>
|
|
Date: Wed, 19 Oct 2022 23:27:10 +0000
|
|
Subject: [PATCH] Verify recipient validity at RCPT command in LMTP runner (2nd
|
|
try)
|
|
|
|
Verify recipient validity at RCPT command in LMTP runner (reviewed merge request c2ddff05a4f405fa46fb792cc69912829c1cbf83, rejected 2 years ago)
|
|
* returning '250 ok' to correctly accept valid recipients
|
|
* reorganized code a bit to better match existing recipient verification code in handle_DATA()
|
|
* fixed unit tests (verified with `tox -e py39-nocov` on Debian/bullseye)
|
|
|
|
See !671 for the original merge request by @foxcpp. Also see !126.
|
|
|
|
Fixes #14
|
|
---
|
|
src/mailman/docs/NEWS.rst | 5 +++
|
|
src/mailman/docs/mta.rst | 38 ++++++++++++++++++++++
|
|
src/mailman/runners/lmtp.py | 38 ++++++++++++++++++++++
|
|
src/mailman/runners/tests/test_lmtp.py | 45 ++++++++++++++++++++------
|
|
4 files changed, 117 insertions(+), 9 deletions(-)
|
|
|
|
diff --git a/src/mailman/docs/mta.rst b/src/mailman/docs/mta.rst
|
|
index bd2df65948..cc85910b2c 100644
|
|
--- a/src/mailman/docs/mta.rst
|
|
+++ b/src/mailman/docs/mta.rst
|
|
@@ -253,6 +253,8 @@ which are local, you may need ``local_recipient_maps`` as above. Note that
|
|
these can be ``regexp`` tables rather than ``hash`` tables. See the
|
|
``Transport maps`` section above.
|
|
|
|
+Starting with version 3.3.6, it is possible to use Mailman's LMTP
|
|
+service with Postfix' ``reject_unverified_recipient``.
|
|
|
|
Postfix documentation
|
|
---------------------
|
|
@@ -381,6 +383,42 @@ two lines to the ``mailman3_transport:`` section.
|
|
headers_remove = message-id
|
|
headers_add = "Message-ID: ${if def:header_message-id:{$h_message-id:}{<E${message_exim_id}@${qualify_domain}>}}"
|
|
|
|
+Alternative setup using callout verification
|
|
+--------------------------------------------
|
|
+
|
|
+Starting with version 3.3.6, you can rely on Mailman's responce on
|
|
+``RCPT TO:`` LMTP command if mailman would accept the recipient
|
|
+address as valid. This can be used in Exim to validate recipients
|
|
+using callout verification.
|
|
+::
|
|
+
|
|
+ # /etc/exim4/conf.d/main/25_mm3_macros
|
|
+ # The colon-separated list of domains served by Mailman.
|
|
+ domainlist mm_domains = list.example.net
|
|
+ # The port of your Mailman's LMTP service
|
|
+ MM3_LMTP_PORT = 8024
|
|
+
|
|
+ # /etc/exim4/local_rcpt_callout (create file or append if already exists)
|
|
+ # Make callout verification for all domains served by Mailman.
|
|
+ *@+mm_domains
|
|
+
|
|
+ # /etc/exim4/conf.d/router/455_mm3_router
|
|
+ mailman3_router:
|
|
+ driver = accept
|
|
+ domains = +mm_domains
|
|
+ # no further conditions, valid recipients are verified in
|
|
+ # acl_check_rcpt using callout verification
|
|
+ transport = mailman3_transport
|
|
+
|
|
+ # /etc/exim4/conf.d/transport/55_mm3_transport
|
|
+ mailman3_transport:
|
|
+ driver = smtp
|
|
+ protocol = lmtp
|
|
+ allow_localhost
|
|
+ hosts = localhost
|
|
+ port = MM3_LMTP_PORT
|
|
+ rcpt_include_affixes = true
|
|
+
|
|
Exim 4 documentation
|
|
--------------------
|
|
|
|
diff --git a/src/mailman/runners/lmtp.py b/src/mailman/runners/lmtp.py
|
|
index 53318f7120..b5b5b181ef 100644
|
|
--- a/src/mailman/runners/lmtp.py
|
|
+++ b/src/mailman/runners/lmtp.py
|
|
@@ -126,6 +126,44 @@ def split_recipient(address):
|
|
|
|
|
|
class LMTPHandler:
|
|
+ @asyncio.coroutine
|
|
+ @transactional
|
|
+ def handle_RCPT(self, server, session, envelope, to, rcpt_options):
|
|
+ listnames = set(getUtility(IListManager).names)
|
|
+ try:
|
|
+ to = parseaddr(to)[1].lower()
|
|
+ local, subaddress, domain = split_recipient(to)
|
|
+ if subaddress is not None:
|
|
+ # Check that local-subaddress is not an actual list name.
|
|
+ listname = '{}-{}@{}'.format(local, subaddress, domain)
|
|
+ if listname in listnames:
|
|
+ local = '{}-{}'.format(local, subaddress)
|
|
+ subaddress = None
|
|
+ listname = '{}@{}'.format(local, domain)
|
|
+ if listname not in listnames:
|
|
+ return ERR_550
|
|
+ canonical_subaddress = SUBADDRESS_NAMES.get(subaddress)
|
|
+ if subaddress is None:
|
|
+ # The message is destined for the mailing list.
|
|
+ # nothing to do here, just keep code similar to handle_DATA
|
|
+ pass
|
|
+ elif canonical_subaddress is None:
|
|
+ # The subaddress was bogus.
|
|
+ slog.error('unknown sub-address: %s', subaddress)
|
|
+ return ERR_550
|
|
+ else:
|
|
+ # A valid subaddress.
|
|
+ # nothing to do here, just keep code similar to handle_DATA
|
|
+ pass
|
|
+ # recipient validated, just do the same as aiosmtpd.LMTP would do
|
|
+ envelope.rcpt_tos.append(to)
|
|
+ envelope.rcpt_options.extend(rcpt_options)
|
|
+ return '250 Ok'
|
|
+ except Exception:
|
|
+ slog.exception('Address verification: %s', to)
|
|
+ config.db.abort()
|
|
+ return ERR_550
|
|
+
|
|
@asyncio.coroutine
|
|
@transactional
|
|
def handle_DATA(self, server, session, envelope):
|
|
diff --git a/src/mailman/runners/tests/test_lmtp.py b/src/mailman/runners/tests/test_lmtp.py
|
|
index 3d0f8e0e5d..5200228028 100644
|
|
--- a/src/mailman/runners/tests/test_lmtp.py
|
|
+++ b/src/mailman/runners/tests/test_lmtp.py
|
|
@@ -114,7 +114,7 @@ Message-ID: <ant>
|
|
|
|
def test_nonexistent_mailing_list(self):
|
|
# Trying to post to a nonexistent mailing list is an error.
|
|
- with self.assertRaises(smtplib.SMTPDataError) as cm:
|
|
+ with self.assertRaises(smtplib.SMTPRecipientsRefused) as cm:
|
|
self._lmtp.sendmail('anne@example.com',
|
|
['notalist@example.com'], """\
|
|
From: anne.person@example.com
|
|
@@ -123,13 +123,22 @@ Subject: An interesting message
|
|
Message-ID: <aardvark>
|
|
|
|
""")
|
|
- self.assertEqual(cm.exception.smtp_code, 550)
|
|
- self.assertEqual(cm.exception.smtp_error,
|
|
+ # smtplib.SMTPRecipientsRefused.args contains a list of errors (for
|
|
+ # each RCPT TO), thus we should have only one error
|
|
+ self.assertEqual(len(cm.exception.args), 1)
|
|
+ args0 = cm.exception.args[0]
|
|
+ # each error should be a dict with the corresponding email address
|
|
+ # as key
|
|
+ self.assertTrue('notalist@example.com' in args0)
|
|
+ errorval = args0['notalist@example.com']
|
|
+ # errorval must be a tuple of (code, errorstr)
|
|
+ self.assertEqual(errorval[0], 550)
|
|
+ self.assertEqual(errorval[1],
|
|
b'Requested action not taken: mailbox unavailable')
|
|
|
|
def test_nonexistent_domain(self):
|
|
# Trying to post to a nonexistent domain is an error.
|
|
- with self.assertRaises(smtplib.SMTPDataError) as cm:
|
|
+ with self.assertRaises(smtplib.SMTPRecipientsRefused) as cm:
|
|
self._lmtp.sendmail('anne@example.com',
|
|
['test@x.example.com'], """\
|
|
From: anne.person@example.com
|
|
@@ -138,8 +147,17 @@ Subject: An interesting message
|
|
Message-ID: <aardvark>
|
|
|
|
""")
|
|
- self.assertEqual(cm.exception.smtp_code, 550)
|
|
- self.assertEqual(cm.exception.smtp_error,
|
|
+ # smtplib.SMTPRecipientsRefused.args contains a list of errors (for
|
|
+ # each RCPT TO), thus we should have only one error
|
|
+ self.assertEqual(len(cm.exception.args), 1)
|
|
+ args0 = cm.exception.args[0]
|
|
+ # each error should be a dict with the corresponding email address
|
|
+ # as key
|
|
+ self.assertTrue('test@x.example.com' in args0)
|
|
+ errorval = args0['test@x.example.com']
|
|
+ # errorval must be a tuple of (code, errorstr)
|
|
+ self.assertEqual(errorval[0], 550)
|
|
+ self.assertEqual(errorval[1],
|
|
b'Requested action not taken: mailbox unavailable')
|
|
|
|
def test_alias_domain(self):
|
|
@@ -168,7 +186,7 @@ X-MailFrom: anne@example.com
|
|
|
|
def test_missing_subaddress(self):
|
|
# Trying to send a message to a bogus subaddress is an error.
|
|
- with self.assertRaises(smtplib.SMTPDataError) as cm:
|
|
+ with self.assertRaises(smtplib.SMTPRecipientsRefused) as cm:
|
|
self._lmtp.sendmail('anne@example.com',
|
|
['test-bogus@example.com'], """\
|
|
From: anne.person@example.com
|
|
@@ -177,8 +195,17 @@ Subject: An interesting message
|
|
Message-ID: <aardvark>
|
|
|
|
""")
|
|
- self.assertEqual(cm.exception.smtp_code, 550)
|
|
- self.assertEqual(cm.exception.smtp_error,
|
|
+ # smtplib.SMTPRecipientsRefused.args contains a list of errors (for
|
|
+ # each RCPT TO), thus we should have only one error
|
|
+ self.assertEqual(len(cm.exception.args), 1)
|
|
+ args0 = cm.exception.args[0]
|
|
+ # each error should be a dict with the corresponding email address
|
|
+ # as key
|
|
+ self.assertTrue('test-bogus@example.com' in args0)
|
|
+ errorval = args0['test-bogus@example.com']
|
|
+ # errorval must be a tuple of (code, errorstr)
|
|
+ self.assertEqual(errorval[0], 550)
|
|
+ self.assertEqual(errorval[1],
|
|
b'Requested action not taken: mailbox unavailable')
|
|
|
|
def test_mailing_list_with_subaddress(self):
|
|
--
|
|
GitLab
|
|
|