Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-aiosmtplib for
openSUSE:Factory checked in at 2026-06-15 19:41:54
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-aiosmtplib (Old)
and /work/SRC/openSUSE:Factory/.python-aiosmtplib.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-aiosmtplib"
Mon Jun 15 19:41:54 2026 rev:17 rq:1359217 version:5.1.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-aiosmtplib/python-aiosmtplib.changes
2026-03-27 16:54:03.238867644 +0100
+++
/work/SRC/openSUSE:Factory/.python-aiosmtplib.new.1981/python-aiosmtplib.changes
2026-06-15 19:44:49.194873975 +0200
@@ -1,0 +2,17 @@
+Sun Jun 14 09:27:07 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 5.1.1 (CVE-2026-53533):
+ * Security: Reject control characters (the C0 range 0x00-0x1F
+ and DEL 0x7F, including CR, LF, and NUL) in SMTP command
+ arguments, preventing command injection via input passed to
+ mail(), rcpt(), vrfy(), expn() or sendmail(). Such input now
+ raises ValueError before anything is written to the
+ connection. More details: https://github.com/cole/aiosmtplib/
+ security/advisories/GHSA-v3q9-hj7j-63hq Thanks to
+ @tonghuaroot for the report.
+ * Bugfix: SMTP.quit() no longer hangs until the read timeout
+ when the peer drops the transport with an exception after
+ QUIT is sent but before the 221 reply is parsed (e.g. AWS SES
+ closing TLS without close_notify).
+
+-------------------------------------------------------------------
Old:
----
aiosmtplib-5.1.0.tar.gz
New:
----
aiosmtplib-5.1.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-aiosmtplib.spec ++++++
--- /var/tmp/diff_new_pack.B0lp86/_old 2026-06-15 19:44:50.506928958 +0200
+++ /var/tmp/diff_new_pack.B0lp86/_new 2026-06-15 19:44:50.510929125 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-aiosmtplib
-Version: 5.1.0
+Version: 5.1.1
Release: 0
Summary: Python asyncio SMTP client
License: MIT
++++++ aiosmtplib-5.1.0.tar.gz -> aiosmtplib-5.1.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/.gitignore
new/aiosmtplib-5.1.1/.gitignore
--- old/aiosmtplib-5.1.0/.gitignore 2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.1/.gitignore 2020-02-02 01:00:00.000000000 +0100
@@ -29,3 +29,4 @@
venv/
poetry.lock
uv.lock
+__pycache__
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/.pre-commit-config.yaml
new/aiosmtplib-5.1.1/.pre-commit-config.yaml
--- old/aiosmtplib-5.1.0/.pre-commit-config.yaml 2020-02-02
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.1/.pre-commit-config.yaml 2020-02-02
01:00:00.000000000 +0100
@@ -12,7 +12,7 @@
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.14.10"
+ rev: "v0.15.12"
hooks:
- id: ruff-check
- id: ruff-format
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/CHANGELOG.rst
new/aiosmtplib-5.1.1/CHANGELOG.rst
--- old/aiosmtplib-5.1.0/CHANGELOG.rst 2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.1/CHANGELOG.rst 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,22 @@
Changelog
=========
+5.1.1
+-----
+
+- Security: Reject control characters (the C0 range ``0x00``-``0x1F`` and DEL
+ ``0x7F``, including CR, LF, and NUL) in SMTP command arguments, preventing
+ command injection via input passed to ``mail()``, ``rcpt()``, ``vrfy()``,
+ ``expn()`` or ``sendmail()``. Such input now raises ``ValueError`` before
+ anything is written to the connection.
+ More details:
https://github.com/cole/aiosmtplib/security/advisories/GHSA-v3q9-hj7j-63hq
+ Thanks to @tonghuaroot for the report.
+- Bugfix: ``SMTP.quit()`` no longer hangs until the read timeout when the
+ peer drops the transport with an exception after ``QUIT`` is sent but
+ before the 221 reply is parsed (e.g. AWS SES closing TLS without
+ ``close_notify``).
+
+
5.1.0
-----
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/PKG-INFO
new/aiosmtplib-5.1.1/PKG-INFO
--- old/aiosmtplib-5.1.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: aiosmtplib
-Version: 5.1.0
+Version: 5.1.1
Summary: asyncio SMTP client
Project-URL: Documentation, https://aiosmtplib.readthedocs.io/en/stable/
Project-URL: Changelog,
https://github.com/cole/aiosmtplib/blob/main/CHANGELOG.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/src/aiosmtplib/__init__.py
new/aiosmtplib-5.1.1/src/aiosmtplib/__init__.py
--- old/aiosmtplib-5.1.0/src/aiosmtplib/__init__.py 2020-02-02
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.1/src/aiosmtplib/__init__.py 2020-02-02
01:00:00.000000000 +0100
@@ -14,6 +14,7 @@
from .errors import (
SMTPAuthenticationError,
SMTPConnectError,
+ SMTPConnectResponseError,
SMTPConnectTimeoutError,
SMTPDataError,
SMTPException,
@@ -26,15 +27,13 @@
SMTPSenderRefused,
SMTPServerDisconnected,
SMTPTimeoutError,
- SMTPConnectResponseError,
)
from .response import SMTPResponse
from .smtp import SMTP
from .typing import SMTPStatus, SMTPTokenGenerator
-
__title__ = "aiosmtplib"
-__version__ = "5.1.0"
+__version__ = "5.1.1"
__author__ = "Cole Maclean"
__license__ = "MIT"
__copyright__ = "Copyright 2022 Cole Maclean"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/src/aiosmtplib/protocol.py
new/aiosmtplib-5.1.1/src/aiosmtplib/protocol.py
--- old/aiosmtplib-5.1.0/src/aiosmtplib/protocol.py 2020-02-02
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.1/src/aiosmtplib/protocol.py 2020-02-02
01:00:00.000000000 +0100
@@ -25,6 +25,8 @@
MAX_LINE_LENGTH = 8192
LINE_ENDINGS_REGEX = re.compile(rb"(?:\r\n|\n|\r(?!\n))")
PERIOD_REGEX = re.compile(rb"(?m)^\.")
+# Reject all C0 controls + DEL; CR/LF/NUL in particular enable injection.
+COMMAND_INJECTION_REGEX = re.compile(rb"[\x00-\x1f\x7f]")
class FlowControlMixin(asyncio.Protocol):
@@ -132,12 +134,15 @@
def connection_lost(self, exc: Exception | None) -> None:
super().connection_lost(exc)
- if not self._quit_sent:
- smtp_exc = SMTPServerDisconnected("Connection lost")
- if exc:
- smtp_exc.__cause__ = exc
-
- if self._response_waiter and not self._response_waiter.done():
+ if self._response_waiter and not self._response_waiter.done():
+ if self._quit_sent:
+ self._response_waiter.set_result(
+ SMTPResponse(SMTPStatus.closing.value, "")
+ )
+ else:
+ smtp_exc = SMTPServerDisconnected("Connection lost")
+ if exc:
+ smtp_exc.__cause__ = exc
self._response_waiter.set_exception(smtp_exc)
self.transport = None
@@ -288,6 +293,9 @@
Sends an SMTP command along with any args to the server, and returns
a response.
"""
+ for arg in args:
+ if COMMAND_INJECTION_REGEX.search(arg):
+ raise ValueError("Command arg contains a prohibited control
character")
if self._command_lock is None:
raise SMTPServerDisconnected("Server not connected")
command = b" ".join(args) + b"\r\n"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/tests/test_commands.py
new/aiosmtplib-5.1.1/tests/test_commands.py
--- old/aiosmtplib-5.1.0/tests/test_commands.py 2020-02-02 01:00:00.000000000
+0100
+++ new/aiosmtplib-5.1.1/tests/test_commands.py 2020-02-02 01:00:00.000000000
+0100
@@ -2,6 +2,9 @@
Lower level SMTP command tests.
"""
+import contextlib
+import email.errors
+import email.message
from typing import Any
import pytest
@@ -18,16 +21,16 @@
from .smtpd import (
RecordingHandler,
- mock_response_done,
+ mock_response_bad_command_sequence,
mock_response_bad_data,
+ mock_response_done,
mock_response_ehlo_full,
mock_response_expn,
mock_response_gibberish,
- mock_response_unavailable,
- mock_response_unrecognized_command,
- mock_response_bad_command_sequence,
mock_response_syntax_error,
mock_response_syntax_error_and_cleanup,
+ mock_response_unavailable,
+ mock_response_unrecognized_command,
)
@@ -433,14 +436,70 @@
assert response.code == SMTPStatus.completed
-async def test_header_injection(
[email protected]("command", ("mail", "rcpt", "vrfy", "expn"))
+async def test_address_command_rejects_injection(
smtp_client: SMTP,
received_commands: list[tuple[str, tuple[Any, ...]]],
+ command: str,
) -> None:
async with smtp_client:
- await smtp_client.mail("[email protected]\r\nX-Malicious-Header: bad
stuff")
+ await smtp_client.ehlo()
+ received_commands.clear()
+
+ method = getattr(smtp_client, command)
+ with pytest.raises(ValueError):
+ await method("[email protected]\r\nRCPT TO:<[email protected]>")
+
+ assert received_commands == []
+
+
+async def test_sendmail_rejects_command_injection(
+ smtp_client: SMTP,
+ received_commands: list[tuple[str, tuple[Any, ...]]],
+ received_messages: list[email.message.EmailMessage],
+) -> None:
+ """
+ A complete transaction smuggled into the sender must never be sent.
+ """
+ injected_sender = (
+ "[email protected]>\r\n"
+ "MAIL FROM:<[email protected]>\r\n"
+ "RCPT TO:<[email protected]>\r\n"
+ "DATA\r\n"
+ "Subject: smuggled\r\n"
+ "\r\n"
+ "smuggled body\r\n"
+ ".\r\n"
+ "RSET\r\nMAIL FROM:<x"
+ )
+ async with smtp_client:
+ await smtp_client.ehlo()
+ received_commands.clear()
+
+ with pytest.raises(ValueError):
+ await smtp_client.sendmail(
+ injected_sender, ["[email protected]"], "Subject:
legit\n\nhi"
+ )
+
+ assert received_commands == []
+ assert received_messages == []
+
+
+async def test_send_message_compat32_does_not_smuggle_envelope_commands(
+ smtp_client: SMTP,
+ received_commands: list[tuple[str, tuple[Any, ...]]],
+) -> None:
+ message = email.message.Message()
+ message["From"] = "[email protected]\r\nRCPT TO:<[email protected]>"
+ message["To"] = "[email protected]"
+ message.set_payload("hello")
+
+ async with smtp_client:
+ # PyPy's BytesGenerator enforces verify_generated_headers and rejects
the
+ # injected newline; CPython's BytesGenerator does not. Either way, no
+ # envelope command may be smuggled.
+ with contextlib.suppress(email.errors.HeaderWriteError):
+ await smtp_client.send_message(message)
- assert len(received_commands) > 0
- for command in received_commands:
- for arg in command:
- assert "bad stuff" not in arg
+ for _, args in received_commands:
+ assert all("hijacker" not in str(arg) for arg in args)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiosmtplib-5.1.0/tests/test_protocol.py
new/aiosmtplib-5.1.1/tests/test_protocol.py
--- old/aiosmtplib-5.1.0/tests/test_protocol.py 2020-02-02 01:00:00.000000000
+0100
+++ new/aiosmtplib-5.1.1/tests/test_protocol.py 2020-02-02 01:00:00.000000000
+0100
@@ -61,7 +61,7 @@
monkeypatch.setattr("aiosmtplib.protocol.MAX_LINE_LENGTH", 128)
with pytest.raises(SMTPResponseException) as exc_info:
- await protocol.execute_command(b"TEST\n", timeout=1.0) # type: ignore
+ await protocol.execute_command(b"TEST", timeout=1.0) # type: ignore
assert exc_info.value.code == 500
assert "Response too long" in exc_info.value.message
@@ -254,7 +254,7 @@
monkeypatch.setattr(protocol, "_response_waiter", None)
with pytest.raises(SMTPServerDisconnected):
- await protocol.execute_command(b"TEST\n", timeout=1.0) # type: ignore
+ await protocol.execute_command(b"TEST", timeout=1.0) # type: ignore
server.close()
await cleanup_server(server)
@@ -287,7 +287,7 @@
_, protocol = await asyncio.wait_for(connect_future, timeout=1.0)
- response = await protocol.execute_command(b"TEST\n", timeout=1.0) # type:
ignore
+ response = await protocol.execute_command(b"TEST", timeout=1.0) # type:
ignore
assert response.code == 220
assert response.message == "Hi"
@@ -348,8 +348,8 @@
)
_, protocol = await asyncio.wait_for(connect_future, timeout=1.0)
- await protocol.execute_command(b"HELO\n", timeout=1.0)
- await protocol.execute_command(b"QUIT\n", timeout=1.0)
+ await protocol.execute_command(b"HELO", timeout=1.0)
+ await protocol.execute_command(b"QUIT", timeout=1.0)
del protocol
# Force garbage collection
@@ -374,7 +374,7 @@
protocol = SMTPProtocol(event_loop)
with pytest.raises(SMTPServerDisconnected):
- await protocol.execute_command(b"TEST\n")
+ await protocol.execute_command(b"TEST")
with pytest.raises(SMTPServerDisconnected):
await protocol.execute_data_command(b"TEST\n")
@@ -480,3 +480,105 @@
with pytest.raises(ConnectionResetError):
await flow_control._drain_helper()
+
+
+class _FakeTransport(asyncio.Transport):
+ """Minimal transport stub for driving SMTPProtocol callbacks directly."""
+
+ def __init__(self) -> None:
+ super().__init__()
+ self._extra: dict[str, object] = {"sslcontext": object()}
+
+ def get_extra_info(self, name: str, default: object = None) -> object:
+ return self._extra.get(name, default)
+
+ def is_closing(self) -> bool:
+ return False
+
+
+async def test_protocol_connection_lost_after_quit_resolves_waiter() -> None:
+ """
+ Regression test for https://github.com/cole/aiosmtplib/issues/345.
+
+ When the peer drops the transport with an exception after ``QUIT\\r\\n``
+ has been written but before the 221 reply is parsed (e.g. AWS SES closes
+ TLS without ``close_notify``, surfaced as
``connection_lost(SSLEOFError)``),
+ the response waiter must be resolved so that ``SMTP.quit()`` returns
+ promptly instead of blocking until the read timeout.
+ """
+ protocol = SMTPProtocol()
+ protocol.connection_made(_FakeTransport())
+
+ protocol._quit_sent = True
+ waiter = protocol._response_waiter
+ assert waiter is not None and not waiter.done()
+
+ protocol.connection_lost(ssl.SSLEOFError("EOF occurred in violation of
protocol"))
+
+ assert waiter.done()
+ assert waiter.exception() is None
+ response = waiter.result()
+ assert response.code == 221
+
+
+async def test_protocol_connection_lost_without_quit_raises() -> None:
+ """
+ Without an outstanding QUIT, ``connection_lost`` must continue to
+ surface ``SMTPServerDisconnected`` on the response waiter.
+ """
+ protocol = SMTPProtocol()
+ protocol.connection_made(_FakeTransport())
+
+ waiter = protocol._response_waiter
+ assert waiter is not None and not waiter.done()
+
+ protocol.connection_lost(ConnectionResetError("boom"))
+
+ assert waiter.done()
+ exc = waiter.exception()
+ assert isinstance(exc, SMTPServerDisconnected)
+
+
+class _WriteRecordingTransport(_FakeTransport):
+ def __init__(self) -> None:
+ super().__init__()
+ self.writes: list[bytes] = []
+
+ def write(self, data: bytes) -> None:
+ self.writes.append(data)
+
+
[email protected](
+ "arg",
+ (
+ b"FROM:<[email protected]\r\nRCPT TO:<[email protected]>",
+ b"FROM:<[email protected]\rRCPT TO:<[email protected]>",
+ b"FROM:<[email protected]\nRCPT TO:<[email protected]>",
+ b"FROM:<[email protected]\x00>",
+ b"FROM:<[email protected]\tEVIL>",
+ b"FROM:<[email protected]\x7f>",
+ ),
+ ids=("crlf", "cr", "lf", "nul", "tab", "del"),
+)
+async def test_protocol_execute_command_rejects_injected_args(arg: bytes) ->
None:
+ protocol = SMTPProtocol()
+ transport = _WriteRecordingTransport()
+ protocol.connection_made(transport)
+
+ with pytest.raises(ValueError, match="prohibited"):
+ await protocol.execute_command(b"MAIL", arg, timeout=1.0)
+
+ assert transport.writes == []
+
+
+async def test_protocol_execute_command_rejects_injected_option() -> None:
+ protocol = SMTPProtocol()
+ transport = _WriteRecordingTransport()
+ protocol.connection_made(transport)
+
+ with pytest.raises(ValueError, match="prohibited"):
+ await protocol.execute_command(
+ b"MAIL", b"FROM:<[email protected]>", b"BODY=8BITMIME\r\nDATA",
timeout=1.0
+ )
+
+ assert transport.writes == []