-
Notifications
You must be signed in to change notification settings - Fork 824
refactor: smtpd->aiosmtpd #8805
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rjsparks
merged 4 commits into
ietf-tools:main
from
jennifer-richards:refactor-test-smtpserver
Apr 21, 2025
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ccce36d
refactor: smtpd -> aiosmtpd
jennifer-richards 025878a
test: set mock return value for EmailOnFailureCommandTests
jennifer-richards 5bb3700
test: increase SMTP.line_length_limit
jennifer-richards aae841b
Merge branch 'main' into refactor-test-smtpserver
jennifer-richards File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,92 +1,56 @@ | ||
| # Copyright The IETF Trust 2014-2020, All Rights Reserved | ||
| # Copyright The IETF Trust 2014-2025, All Rights Reserved | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| from aiosmtpd.controller import Controller | ||
| from aiosmtpd.smtp import SMTP | ||
| from email.utils import parseaddr | ||
| from typing import Optional | ||
|
|
||
| import smtpd | ||
| import threading | ||
| import asyncore | ||
|
|
||
| import debug # pyflakes:ignore | ||
| class SMTPTestHandler: | ||
|
|
||
| class AsyncCoreLoopThread(object): | ||
| def __init__(self, inbox: list): | ||
| self.inbox = inbox | ||
|
|
||
| def wrap_loop(self, exit_condition, timeout=1.0, use_poll=False, map=None): | ||
| if map is None: | ||
| map = asyncore.socket_map | ||
| while map and not exit_condition: | ||
| asyncore.loop(timeout=1.0, use_poll=False, map=map, count=1) | ||
| async def handle_DATA(self, server, session, envelope): | ||
| """Handle the DATA command and 'deliver' the message""" | ||
|
|
||
| def start(self): | ||
| """Start the listening service""" | ||
| self.exit_condition = [] | ||
| kwargs={'exit_condition':self.exit_condition,'timeout':1.0} | ||
| self.thread = threading.Thread(target=self.wrap_loop, kwargs=kwargs) | ||
| self.thread.daemon = True | ||
| self.thread.daemon = True | ||
| self.thread.start() | ||
|
|
||
| def stop(self): | ||
| """Stop the listening service""" | ||
| self.exit_condition.append(True) | ||
| self.thread.join() | ||
|
|
||
|
|
||
| class SMTPTestChannel(smtpd.SMTPChannel): | ||
| self.inbox.append(envelope.content) | ||
| # Per RFC2033: https://datatracker.ietf.org/doc/html/rfc2033.html#section-4.2 | ||
| # ...after the final ".", the server returns one reply | ||
| # for each previously successful RCPT command in the mail transaction, | ||
| # in the order that the RCPT commands were issued. Even if there were | ||
| # multiple successful RCPT commands giving the same forward-path, there | ||
| # must be one reply for each successful RCPT command. | ||
| return "\n".join("250 OK" for _ in envelope.rcpt_tos) | ||
|
|
||
| # mail_options = ['BODY=8BITMIME', 'SMTPUTF8'] | ||
|
|
||
| def smtp_RCPT(self, arg): | ||
| if not self.mailfrom: | ||
| self.push(str('503 Error: need MAIL command')) | ||
| return | ||
| arg = self._strip_command_keyword('TO:', arg) | ||
| address, __ = self._getaddr(arg) | ||
| if not address: | ||
| self.push(str('501 Syntax: RCPT TO: <address>')) | ||
| return | ||
| async def handle_RCPT(self, server, session, envelope, address, rcpt_options): | ||
| """Handle an RCPT command and add the address to the envelope if it is acceptable""" | ||
| _, address = parseaddr(address) | ||
| if address == "": | ||
| return "501 Syntax: RCPT TO: <address>" | ||
| if "poison" in address: | ||
| self.push(str('550 Error: Not touching that')) | ||
| return | ||
| self.rcpt_options = [] | ||
| self.rcpttos.append(address) | ||
| self.push(str('250 Ok')) | ||
|
|
||
| class SMTPTestServer(smtpd.SMTPServer): | ||
|
|
||
| def __init__(self,localaddr,remoteaddr,inbox): | ||
| if inbox is not None: | ||
| self.inbox=inbox | ||
| else: | ||
| self.inbox = [] | ||
| smtpd.SMTPServer.__init__(self,localaddr,remoteaddr) | ||
| return "550 Error: Not touching that" | ||
| # At this point the address is acceptable | ||
| envelope.rcpt_tos.append(address) | ||
| return "250 OK" | ||
|
|
||
| def handle_accept(self): | ||
| pair = self.accept() | ||
| if pair is not None: | ||
| conn, addr = pair | ||
| #channel = SMTPTestChannel(self, conn, addr) | ||
| SMTPTestChannel(self, conn, addr) | ||
|
|
||
| def process_message(self, peer, mailfrom, rcpttos, data, mail_options=None, rcpt_options=None): | ||
| self.inbox.append(data) | ||
| class SMTPTestServerDriver: | ||
|
|
||
|
|
||
| class SMTPTestServerDriver(object): | ||
| def __init__(self, localaddr, remoteaddr, inbox=None): | ||
| self.localaddr=localaddr | ||
| self.remoteaddr=remoteaddr | ||
| if inbox is not None: | ||
| self.inbox = inbox | ||
| else: | ||
| self.inbox = [] | ||
| self.thread_driver = None | ||
| def __init__(self, address: str, port: int, inbox: Optional[list] = None): | ||
| # Allow longer lines than the 1001 that RFC 5321 requires. As of 2025-04-16 the | ||
| # datatracker emits some non-compliant messages. | ||
| # See https://aiosmtpd.aio-libs.org/en/latest/smtp.html | ||
| SMTP.line_length_limit = 4000 # tests start failing between 3000 and 4000 | ||
| self.controller = Controller( | ||
| hostname=address, | ||
| port=port, | ||
| handler=SMTPTestHandler(inbox=[] if inbox is None else inbox), | ||
| ) | ||
|
|
||
| def start(self): | ||
| self.smtpserver = SMTPTestServer(self.localaddr,self.remoteaddr,self.inbox) | ||
| self.thread_driver = AsyncCoreLoopThread() | ||
| self.thread_driver.start() | ||
| self.controller.start() | ||
|
|
||
| def stop(self): | ||
| if self.thread_driver: | ||
| self.thread_driver.stop() | ||
|
|
||
| self.controller.stop() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO moving forward, we should try to separate test only dependencies. Not saying we should start with this PR, but better to start somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will help us to have clear SBOM for production server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea, though have some concerns about how we would validate changes to the production dependency list prior to deployment.
I don't think it's practical to start in this PR because there will be quite a few tooling and deployment adjustments needed regardless of how we do it. It'll need to be its own project.