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-08-21 17:01:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-aiosmtplib (Old)
 and      /work/SRC/openSUSE:Factory/.python-aiosmtplib.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-aiosmtplib"

Fri Aug 21 17:01:41 2026 rev:19 rq:1372852 version:5.1.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-aiosmtplib/python-aiosmtplib.changes      
2026-06-22 17:33:32.346363894 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-aiosmtplib.new.1258/python-aiosmtplib.changes
    2026-08-21 17:03:14.124118452 +0200
@@ -1,0 +2,28 @@
+Fri Aug 21 12:21:32 UTC 2026 - Nico Krapp <[email protected]>
+
+- Update to 5.1.2 (fixes CVE-2026-55558, bsc#1275982)
+  * Security: Discard any buffered server data before the STARTTLS handshake,
+    preventing a response-injection attack where a man-in-the-middle pre-stages
+    plaintext that would otherwise be read as part of the encrypted session
+  * Bugfix: Corrected SMTPResponse __repr__ result
+  * Bugfix: Calling connect() on an already-connected client now raises
+    SMTPException instead of deadlocking on the connection lock
+  * Feature: Poe command runner for dev tasks
+  * Bugfix: Timeout ignored during CRAM-MD5 verification
+  * Bugfix: Only parse EHLO response after validating success
+  * Bugfix: return None from extract_sender when address list is empty
+  * Bugfix: Enforce a maximum total response size, preventing unbounded memory
+    use if a server streams data with no line ending or endless multiline
+    continuation lines
+  * Bugfix: Use the invalid_response status code (-1) instead of 500 when a
+    server response line exceeds the maximum length
+  * Bugfix: A read timeout now closes the connection instead of leaving it in
+    a desynced state, where a late server response could be mispaired with a
+    subsequent command
+  * Bugfix: Correctly parse old-style AUTH= extension advertisements; all
+    advertised methods are now kept (e.g. both PLAIN and LOGIN from
+    AUTH=PLAIN LOGIN)
+  * Bugfix: ESMTP extension lines with leading whitespace are no longer ignored
+- enable testsuite again
+
+-------------------------------------------------------------------

Old:
----
  aiosmtplib-5.1.1.tar.gz

New:
----
  aiosmtplib-5.1.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-aiosmtplib.spec ++++++
--- /var/tmp/diff_new_pack.NTXRxg/_old  2026-08-21 17:03:14.956147910 +0200
+++ /var/tmp/diff_new_pack.NTXRxg/_new  2026-08-21 17:03:14.959148016 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-aiosmtplib
-Version:        5.1.1
+Version:        5.1.2
 Release:        0
 Summary:        Python asyncio SMTP client
 License:        MIT
@@ -58,15 +58,7 @@
 %python_expand %fdupes %{buildroot}%{$python_sitelib}
 
 %check
-# Disable tests, broken with pytest-asyncio >= 1.0.0
-# gh#cole/aiosmtplib@3b4bd0d7048c
-# https://github.com/cole/aiosmtplib/blob/main/requirements-dev.txt#L2
-# %%pytest -rs -k "not test_live and not test_tls"
-
-%{python_expand # just test import meantime tests are broken
-export PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}%{buildroot}%{$python_sitelib}
-$python -c "import aiosmtplib; assert aiosmtplib.__version__ == '%{version}'"
-}
+%pytest -rs -k "not test_live and not test_tls"
 
 %files %{python_files}
 %doc README.rst docs/*.rst

++++++ aiosmtplib-5.1.1.tar.gz -> aiosmtplib-5.1.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/.circleci/config.yml 
new/aiosmtplib-5.1.2/.circleci/config.yml
--- old/aiosmtplib-5.1.1/.circleci/config.yml   2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/.circleci/config.yml   2020-02-02 01:00:00.000000000 
+0100
@@ -1,7 +1,7 @@
 version: 2.1
 
 orbs:
-  codecov: codecov/[email protected]
+  codecov: codecov/[email protected]
 
 executors:
   cpython_executor:
@@ -45,29 +45,24 @@
     executor:
       name: cpython_executor
     steps:
+      - checkout
       - attach_workspace:
           at: dist
-      - run: uv tool run check-wheel-contents dist/*.whl
-      - run: uv tool run twine check --strict dist/*
+      - run: uv tool run --from poethepoet poe buildcheck
 
   typecheck:
     executor:
       name: cpython_executor
     steps:
       - checkout
-      - run: uv tool run mypy src/aiosmtplib
-      - run: uv tool run --with . mypy tests/type_usage.py
-      - run: uv tool run pyright src/aiosmtplib
-      - run: uv tool run --with . pyright tests/type_usage.py
-      - run: uv tool run ty check src/aiosmtplib
-      - run: uv tool run --with . ty check tests/type_usage.py
+      - run: uv tool run --from poethepoet poe typecheck
 
   security:
     executor:
       name: cpython_executor
     steps:
       - checkout
-      - run: uv tool run bandit -n 10 -x tests -r src/aiosmtplib
+      - run: uv tool run --from poethepoet poe security
 
   docs:
     executor:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/.pre-commit-config.yaml 
new/aiosmtplib-5.1.2/.pre-commit-config.yaml
--- old/aiosmtplib-5.1.1/.pre-commit-config.yaml        2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/.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.15.12"
+    rev: "v0.15.15"
     hooks:
       - id: ruff-check
       - id: ruff-format
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/CHANGELOG.rst 
new/aiosmtplib-5.1.2/CHANGELOG.rst
--- old/aiosmtplib-5.1.1/CHANGELOG.rst  2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/CHANGELOG.rst  2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,35 @@
 Changelog
 =========
 
+5.1.2
+-----
+
+- Security: Discard any buffered server data before the STARTTLS handshake,
+  preventing a response-injection attack where a man-in-the-middle pre-stages
+  plaintext that would otherwise be read as part of the encrypted session.
+  More details: 
https://github.com/cole/aiosmtplib/security/advisories/GHSA-vxj7-4xrp-5vr4
+- Bugfix: Corrected SMTPResponse ``__repr__`` result
+- Bugfix: Calling ``connect()`` on an already-connected client now raises
+  ``SMTPException`` instead of deadlocking on the connection lock
+- Feature: Poe command runner for dev tasks
+- Bugfix: Timeout ignored during CRAM-MD5 verification
+- Bugfix: Only parse EHLO response after validating success
+- Bugfix: return None from extract_sender when address list is empty
+- Bugfix: Enforce a maximum total response size, preventing unbounded memory
+  use if a server streams data with no line ending or endless multiline
+  continuation lines
+- Bugfix: Use the invalid_response status code (-1) instead of 500 when a
+  server response line exceeds the maximum length
+- Bugfix: A read timeout now closes the connection instead of leaving it in a
+  desynced state, where a late server response could be mispaired with a
+  subsequent command
+- Bugfix: Correctly parse old-style ``AUTH=`` extension advertisements; all
+  advertised methods are now kept (e.g. both PLAIN and LOGIN from
+  ``AUTH=PLAIN LOGIN``)
+- Bugfix: ESMTP extension lines with leading whitespace are no
+  longer ignored
+
+
 5.1.1
 -----
 
@@ -16,7 +45,6 @@
   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.1/PKG-INFO 
new/aiosmtplib-5.1.2/PKG-INFO
--- old/aiosmtplib-5.1.1/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: aiosmtplib
-Version: 5.1.1
+Version: 5.1.2
 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.1/pyproject.toml 
new/aiosmtplib-5.1.2/pyproject.toml
--- old/aiosmtplib-5.1.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -43,6 +43,45 @@
 [tool.hatch.version]
 path = "src/aiosmtplib/__init__.py"
 
+[tool.poe.tasks]
+lint = { cmd = "uvx ruff check src tests", help = "Lint with ruff" }
+format = { cmd = "uvx ruff format src tests", help = "Format with ruff" }
+format-check = { cmd = "uvx ruff format --check src tests", help = "Check 
formatting (no writes)" }
+security = { cmd = "uvx bandit -n 10 -x tests -r src/aiosmtplib", help = "Scan 
for security issues with bandit" }
+test = { cmd = "pytest", help = "Run the test suite" }
+coverage-report = { cmd = "uvx coverage report --fail-under=100", help = 
"Report coverage (fails under 100%)" }
+docs = { cmd = "uv run --with-requirements docs/requirements.txt --with . 
python -m sphinx -nWT -b html docs docs/build/html", help = "Build the HTML 
docs" }
+
+[tool.poe.tasks.typecheck-mypy]
+help = "Type check with mypy"
+default_item_type = "cmd"
+sequence = ["uvx mypy src/aiosmtplib", "uvx --with . mypy tests/type_usage.py"]
+
+[tool.poe.tasks.typecheck-pyright]
+help = "Type check with pyright"
+default_item_type = "cmd"
+sequence = ["uvx pyright src/aiosmtplib", "uvx --with . pyright 
tests/type_usage.py"]
+
+[tool.poe.tasks.typecheck-ty]
+help = "Type check with ty"
+default_item_type = "cmd"
+sequence = ["uvx ty check src/aiosmtplib", "uvx --with . ty check 
tests/type_usage.py"]
+
+[tool.poe.tasks.typecheck]
+help = "Run all type checkers (mypy, pyright, ty)"
+default_item_type = "ref"
+sequence = ["typecheck-mypy", "typecheck-pyright", "typecheck-ty"]
+
+[tool.poe.tasks.buildcheck]
+help = "Validate built artifacts (run `uv build` first)"
+default_item_type = "cmd"
+sequence = ["uvx check-wheel-contents dist", "uvx twine check --strict dist/*"]
+
+[tool.poe.tasks.check]
+help = "Run all lint/type/security/test checks"
+default_item_type = "ref"
+sequence = ["lint", "format-check", "typecheck", "security", "test"]
+
 [tool.pytest.ini_options]
 pythonpath = "src"
 asyncio_mode = "auto"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/requirements-dev.txt 
new/aiosmtplib-5.1.2/requirements-dev.txt
--- old/aiosmtplib-5.1.1/requirements-dev.txt   2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/requirements-dev.txt   2020-02-02 01:00:00.000000000 
+0100
@@ -6,3 +6,4 @@
 hypothesis>=6.56
 aiosmtpd>=1.4.2
 trustme>=0.9.0
+poethepoet>=0.24
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/src/aiosmtplib/__init__.py 
new/aiosmtplib-5.1.2/src/aiosmtplib/__init__.py
--- old/aiosmtplib-5.1.1/src/aiosmtplib/__init__.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/src/aiosmtplib/__init__.py     2020-02-02 
01:00:00.000000000 +0100
@@ -33,7 +33,7 @@
 from .typing import SMTPStatus, SMTPTokenGenerator
 
 __title__ = "aiosmtplib"
-__version__ = "5.1.1"
+__version__ = "5.1.2"
 __author__ = "Cole Maclean"
 __license__ = "MIT"
 __copyright__ = "Copyright 2022 Cole Maclean"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/src/aiosmtplib/email.py 
new/aiosmtplib-5.1.2/src/aiosmtplib/email.py
--- old/aiosmtplib-5.1.1/src/aiosmtplib/email.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/src/aiosmtplib/email.py        2020-02-02 
01:00:00.000000000 +0100
@@ -141,7 +141,11 @@
     if sender_header is None:
         return None
 
-    return extract_addresses(sender_header)[0]
+    addresses = extract_addresses(sender_header)
+    if not addresses:
+        return None
+
+    return addresses[0]
 
 
 def extract_recipients(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/src/aiosmtplib/esmtp.py 
new/aiosmtplib-5.1.2/src/aiosmtplib/esmtp.py
--- old/aiosmtplib-5.1.1/src/aiosmtplib/esmtp.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/src/aiosmtplib/esmtp.py        2020-02-02 
01:00:00.000000000 +0100
@@ -9,7 +9,6 @@
 
 
 OLDSTYLE_AUTH_REGEX = re.compile(r"auth=(?P<auth>.*)", flags=re.I)
-EXTENSIONS_REGEX = re.compile(r"(?P<ext>[A-Za-z0-9][A-Za-z0-9\-]*) ?")
 
 
 def parse_esmtp_extensions(message: str) -> tuple[dict[str, str], list[str]]:
@@ -41,32 +40,32 @@
     esmtp_extensions: dict[str, str] = {}
     auth_types: list[str] = []
 
-    response_lines = message.split("\n")
+    # Skip the greeting line; each remaining line is "KEYWORD [params]".
+    for line in message.split("\n")[1:]:
+        line = line.strip()
+        if not line:
+            continue
 
-    # ignore the first line
-    for line in response_lines[1:]:
         # To be able to communicate with as many SMTP servers as possible,
-        # we have to take the old-style auth advertisement into account,
-        # because:
-        # 1) Else our SMTP feature parser gets confused.
-        # 2) There are some servers that only advertise the auth methods we
-        #    support using the old style.
-        auth_match = OLDSTYLE_AUTH_REGEX.match(line)
-        if auth_match is not None:
-            auth_type = auth_match.group("auth")
-            auth_types.append(auth_type.lower().strip())
-
-        # RFC 1869 requires a space between ehlo keyword and parameters.
-        # It's actually stricter, in that only spaces are allowed between
-        # parameters, but were not going to check for that here.  Note
-        # that the space isn't present if there are no parameters.
-        extensions = EXTENSIONS_REGEX.match(line)
-        if extensions is not None:
-            extension = extensions.group("ext").lower()
-            params = extensions.string[extensions.end("ext") :].strip()
-            esmtp_extensions[extension] = params
+        # we have to take the old-style "AUTH=method[ method...]" advertisement
+        # into account. Some servers only advertise the auth methods we support
+        # using the old style, so register the extension here too (not just the
+        # methods) to keep supports_extension("auth") accurate.
+        oldstyle_auth = OLDSTYLE_AUTH_REGEX.fullmatch(line)
+        if oldstyle_auth is not None:
+            params = oldstyle_auth["auth"]
+            esmtp_extensions["auth"] = params
+            auth_types.extend(method.lower() for method in params.split())
+            continue
+
+        # RFC 1869 requires a space between the ehlo keyword and its parameters
+        # (and only spaces between parameters, though we don't enforce that).
+        # The space isn't present when there are no parameters.
+        keyword, _, params = line.partition(" ")
+        keyword = keyword.lower()
+        esmtp_extensions[keyword] = params
 
-            if extension == "auth":
-                auth_types.extend([param.strip().lower() for param in 
params.split()])
+        if keyword == "auth":
+            auth_types.extend(method.lower() for method in params.split())
 
     return esmtp_extensions, auth_types
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/src/aiosmtplib/protocol.py 
new/aiosmtplib-5.1.2/src/aiosmtplib/protocol.py
--- old/aiosmtplib-5.1.1/src/aiosmtplib/protocol.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/src/aiosmtplib/protocol.py     2020-02-02 
01:00:00.000000000 +0100
@@ -23,6 +23,10 @@
 
 
 MAX_LINE_LENGTH = 8192
+# A whole response (including multiline continuations) may not exceed this.
+# Bounds memory if a server streams data with no line ending or endless
+# continuation lines; generous over any real EHLO, which is a few KB.
+MAX_RESPONSE_LENGTH = MAX_LINE_LENGTH * 4
 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.
@@ -162,6 +166,15 @@
 
         self._buffer.extend(data)
 
+        if len(self._buffer) > MAX_RESPONSE_LENGTH:
+            del self._buffer[:]
+            self._response_waiter.set_exception(
+                SMTPResponseException(
+                    SMTPStatus.invalid_response.value, "Response too long"
+                )
+            )
+            return
+
         # If we got an obvious partial message, don't try to parse the buffer
         last_linebreak = data.rfind(b"\n")
         if (
@@ -217,7 +230,7 @@
 
             if len(line) > MAX_LINE_LENGTH:
                 raise SMTPResponseException(
-                    SMTPStatus.unrecognized_command, "Response too long"
+                    SMTPStatus.invalid_response.value, "Response too long"
                 )
 
             try:
@@ -366,6 +379,13 @@
             if self.transport is None or self.transport.is_closing():
                 raise SMTPServerDisconnected("Connection lost")
 
+            # STARTTLS injection defense (RFC 3207 section 4.2): a compliant
+            # server sends nothing after its 220 reply until TLS is negotiated.
+            # Any bytes still buffered here are plaintext a MITM may have
+            # injected; discard them so they cannot be misread as part of the
+            # encrypted session once the handshake completes.
+            del self._buffer[:]
+
             try:
                 tls_transport = await self._loop.start_tls(
                     cast(asyncio.WriteTransport, self.transport),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/src/aiosmtplib/response.py 
new/aiosmtplib-5.1.2/src/aiosmtplib/response.py
--- old/aiosmtplib-5.1.1/src/aiosmtplib/response.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/src/aiosmtplib/response.py     2020-02-02 
01:00:00.000000000 +0100
@@ -27,8 +27,5 @@
     code: int
     message: str
 
-    def __repr__(self) -> str:
-        return f"({self.code}, {self.message})"
-
     def __str__(self) -> str:
         return f"{self.code} {self.message}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/src/aiosmtplib/smtp.py 
new/aiosmtplib-5.1.2/src/aiosmtplib/smtp.py
--- old/aiosmtplib-5.1.1/src/aiosmtplib/smtp.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/src/aiosmtplib/smtp.py 2020-02-02 01:00:00.000000000 
+0100
@@ -440,6 +440,11 @@
         self.loop = asyncio.get_running_loop()
         if self._connect_lock is None:
             self._connect_lock = asyncio.Lock()
+
+        if self.is_connected:
+            raise SMTPException("SMTP instance is already connected")
+
+        # The lock is held until close(), serializing concurrent connect calls.
         await self._connect_lock.acquire()
 
         # If we're not using a socket, default to port and hostname
@@ -595,7 +600,10 @@
             response = await self.protocol.execute_command(
                 *args, timeout=self.timeout if timeout is Default.token else 
timeout
             )
-        except SMTPServerDisconnected:
+        except (SMTPServerDisconnected, SMTPTimeoutError):
+            # A read timeout leaves the protocol desynced (a partial buffer and
+            # a server response we gave up on), so close rather than risk
+            # mispairing that response with a later command.
             self.close()
             raise
 
@@ -938,7 +946,7 @@
 
         try:
             return await self.protocol.execute_data_command(message, 
timeout=timeout)
-        except SMTPServerDisconnected:
+        except (SMTPServerDisconnected, SMTPTimeoutError):
             self.close()
             raise
 
@@ -967,11 +975,12 @@
         response = await self.execute_command(
             b"EHLO", hostname.encode("ascii"), timeout=timeout
         )
-        self.last_ehlo_response = response
 
         if response.code != SMTPStatus.completed:
             raise SMTPHeloError(response.code, response.message)
 
+        self.last_ehlo_response = response
+
         return response
 
     def supports_extension(self, extension: str, /) -> bool:
@@ -1067,7 +1076,7 @@
             response = await self.protocol.start_tls(
                 tls_context, server_hostname=server_hostname, timeout=timeout
             )
-        except SMTPServerDisconnected:
+        except (SMTPServerDisconnected, SMTPTimeoutError):
             self.close()
             raise
 
@@ -1165,7 +1174,7 @@
         verification_bytes = auth_crammd5_verify(
             username, password, initial_response.message
         )
-        response = await self.execute_command(verification_bytes)
+        response = await self.execute_command(verification_bytes, 
timeout=timeout)
 
         if response.code != SMTPStatus.auth_successful:
             raise SMTPAuthenticationError(response.code, response.message)
@@ -1337,7 +1346,7 @@
             ...     await smtp.sendmail("[email protected]", recipients, message)
             ...     return await smtp.quit()
             >>> asyncio.run(connect_and_send())
-            (221, Bye)
+            SMTPResponse(code=221, message='Bye')
 
         In the above example, the message was accepted for delivery for all
         three addresses. If delivery had been only successful to two
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/auth.py 
new/aiosmtplib-5.1.2/tests/auth.py
--- old/aiosmtplib-5.1.1/tests/auth.py  2020-02-02 01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/tests/auth.py  2020-02-02 01:00:00.000000000 +0100
@@ -12,6 +12,7 @@
         super().__init__(*args, **kwargs)
 
         self.received_commands: list[bytes] = []
+        self.received_kwargs: list[dict[str, Any]] = []
         self.responses: deque[tuple[int, str]] = deque()
         self.esmtp_extensions = {"auth": ""}
         self.server_auth_methods = ["cram-md5", "login", "plain"]
@@ -19,6 +20,7 @@
 
     async def execute_command(self, *args: Any, **kwargs: Any) -> SMTPResponse:
         self.received_commands.append(b" ".join(args))
+        self.received_kwargs.append(kwargs)
 
         response = self.responses.popleft()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_auth_methods.py 
new/aiosmtplib-5.1.2/tests/test_auth_methods.py
--- old/aiosmtplib-5.1.1/tests/test_auth_methods.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/tests/test_auth_methods.py     2020-02-02 
01:00:00.000000000 +0100
@@ -304,3 +304,22 @@
 
     encoded = auth_xoauth2_encode("[email protected]", "test_oauth_token")
     assert mock_auth.received_commands == [b"AUTH XOAUTH2 " + encoded]
+
+
+async def test_auth_crammd5_passes_timeout(mock_auth: DummySMTPAuth) -> None:
+    """
+    Test that auth_crammd5 passes timeout to the verification command.
+
+    Both execute_command calls in auth_crammd5 should receive the timeout 
parameter.
+    """
+    continue_response = (
+        SMTPStatus.auth_continue,
+        base64.b64encode(b"challenge").decode("utf-8"),
+    )
+    mock_auth.responses.extend([continue_response, SUCCESS_RESPONSE])
+    await mock_auth.auth_crammd5("user", "pass", timeout=5.0)
+
+    # Both commands should have received the timeout
+    assert len(mock_auth.received_kwargs) == 2
+    assert mock_auth.received_kwargs[0].get("timeout") == 5.0
+    assert mock_auth.received_kwargs[1].get("timeout") == 5.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_commands.py 
new/aiosmtplib-5.1.2/tests/test_commands.py
--- old/aiosmtplib-5.1.1/tests/test_commands.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/tests/test_commands.py 2020-02-02 01:00:00.000000000 
+0100
@@ -94,6 +94,25 @@
         assert exception_info.value.code == SMTPStatus.unrecognized_command
 
 
[email protected]_mocks(smtp_EHLO=mock_response_unrecognized_command)
+async def test_ehlo_error_does_not_set_supports_esmtp(smtp_client: SMTP) -> 
None:
+    """
+    Test that a failed EHLO response does not set supports_esmtp to True.
+
+    The EHLO response should only be parsed for extensions after validating
+    that the response code indicates success.
+    """
+    async with smtp_client:
+        assert smtp_client.supports_esmtp is False
+
+        with pytest.raises(SMTPHeloError):
+            await smtp_client.ehlo()
+
+        assert smtp_client.supports_esmtp is False
+        assert smtp_client.last_ehlo_response is None
+        assert smtp_client.esmtp_extensions == {}
+
+
 @pytest.mark.smtpd_mocks(smtp_EHLO=mock_response_ehlo_full)
 async def test_ehlo_parses_esmtp_extensions(smtp_client: SMTP) -> None:
     async with smtp_client:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_connect.py 
new/aiosmtplib-5.1.2/tests/test_connect.py
--- old/aiosmtplib-5.1.1/tests/test_connect.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/tests/test_connect.py  2020-02-02 01:00:00.000000000 
+0100
@@ -69,6 +69,22 @@
         assert response.code == SMTPStatus.completed
 
 
+async def test_connect_while_connected_raises(
+    smtp_client: SMTP, smtpd_server: asyncio.AbstractServer
+) -> None:
+    await smtp_client.connect()
+    assert smtp_client.is_connected
+
+    # Connecting an already-connected client is an error, not a deadlock.
+    with pytest.raises(SMTPException, match="already connected"):
+        await asyncio.wait_for(smtp_client.connect(), 1.0)
+
+    # The existing connection is untouched.
+    assert smtp_client.is_connected
+    response = await smtp_client.noop()
+    assert response.code == SMTPStatus.completed
+
+
 @pytest.mark.smtpd_mocks(_handle_client=mock_response_unavailable)
 async def test_bad_connect_response_raises_error(smtp_client: SMTP) -> None:
     with pytest.raises(SMTPConnectError):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_email_utils.py 
new/aiosmtplib-5.1.2/tests/test_email_utils.py
--- old/aiosmtplib-5.1.1/tests/test_email_utils.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/tests/test_email_utils.py      2020-02-02 
01:00:00.000000000 +0100
@@ -362,3 +362,18 @@
 
     with pytest.raises(ValueError):
         extract_sender(message)
+
+
+def test_extract_sender_empty_address_returns_none() -> None:
+    """
+    Test that extract_sender returns None when the From header contains no 
valid addresses.
+
+    If extract_addresses() returns an empty list (e.g., malformed From header),
+    extract_sender should return None instead of raising IndexError.
+    """
+    message = EmailMessage()
+    # Use undisclosed-recipients which parses to an empty list
+    message["From"] = "undisclosed-recipients:;"
+
+    sender = extract_sender(message)
+    assert sender is None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_esmtp_utils.py 
new/aiosmtplib-5.1.2/tests/test_esmtp_utils.py
--- old/aiosmtplib-5.1.1/tests/test_esmtp_utils.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/aiosmtplib-5.1.2/tests/test_esmtp_utils.py      2020-02-02 
01:00:00.000000000 +0100
@@ -42,10 +42,10 @@
 
 
 def test_auth_type_parsing() -> None:
-    response = """blah blah blah
+    response = """mail.example.com Hello [127.0.0.1]
 AUTH FOO BAR
     """
-    extensions, auth_types = parse_esmtp_extensions(response)
+    _, auth_types = parse_esmtp_extensions(response)
 
     assert "foo" in auth_types
     assert "bar" in auth_types
@@ -53,21 +53,59 @@
 
 
 def test_old_school_auth_type_parsing() -> None:
-    response = """blah blah blah
+    response = """mail.example.com Hello [127.0.0.1]
 AUTH=PLAIN
     """
-    extensions, auth_types = parse_esmtp_extensions(response)
+    _, auth_types = parse_esmtp_extensions(response)
 
     assert "plain" in auth_types
     assert "cram-md5" not in auth_types
 
 
 def test_mixed_auth_type_parsing() -> None:
-    response = """blah blah blah
+    response = """mail.example.com Hello [127.0.0.1]
 AUTH=PLAIN
 AUTH CRAM-MD5
     """
-    extensions, auth_types = parse_esmtp_extensions(response)
+    _, auth_types = parse_esmtp_extensions(response)
 
     assert "plain" in auth_types
     assert "cram-md5" in auth_types
+
+
+def test_old_school_multiple_auth_type_parsing() -> None:
+    response = """mail.example.com Hello [127.0.0.1]
+AUTH=PLAIN LOGIN
+    """
+    extensions, auth_types = parse_esmtp_extensions(response)
+
+    # Every space-separated method in the advertisement is kept.
+    assert auth_types == ["plain", "login"]
+    # login() requires the "auth" extension to be registered for old-style
+    # advertisements, so supports_extension("auth") stays accurate.
+    assert "auth" in extensions
+
+
+def test_old_school_only_auth_no_junk() -> None:
+    response = """mail.example.com Hello [127.0.0.1]
+AUTH=CRAM-MD5
+    """
+    extensions, auth_types = parse_esmtp_extensions(response)
+
+    assert "auth" in extensions
+    assert auth_types == ["cram-md5"]
+
+
+def test_leading_whitespace_extension_parsing() -> None:
+    response = "mail.example.com Hello [127.0.0.1]\n   SIZE 1000"
+    extensions, _ = parse_esmtp_extensions(response)
+
+    assert extensions["size"] == "1000"
+
+
+def test_blank_line_no_empty_keyword() -> None:
+    response = "mail.example.com Hello [127.0.0.1]\n\n8BITMIME\n"
+    extensions, _ = parse_esmtp_extensions(response)
+
+    assert "" not in extensions
+    assert "8bitmime" in extensions
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_protocol.py 
new/aiosmtplib-5.1.2/tests/test_protocol.py
--- old/aiosmtplib-5.1.1/tests/test_protocol.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/tests/test_protocol.py 2020-02-02 01:00:00.000000000 
+0100
@@ -63,7 +63,82 @@
     with pytest.raises(SMTPResponseException) as exc_info:
         await protocol.execute_command(b"TEST", timeout=1.0)  # type: ignore
 
-    assert exc_info.value.code == 500
+    assert exc_info.value.code == -1
+    assert "Response too long" in exc_info.value.message
+
+    server.close()
+    await cleanup_server(server)
+
+
+async def test_protocol_response_no_newline_overrun(
+    bind_address: str,
+    hostname: str,
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    event_loop = asyncio.get_running_loop()
+
+    async def client_connected(
+        reader: asyncio.StreamReader, writer: asyncio.StreamWriter
+    ) -> None:
+        await reader.read(1000)
+        # No line ending at all, so the per-line cap is never reached.
+        writer.write(b"2" * 500)
+        await writer.drain()
+
+    server = await asyncio.start_server(
+        client_connected, host=bind_address, port=0, family=socket.AF_INET
+    )
+    server_port = server.sockets[0].getsockname()[1] if server.sockets else 0
+    connect_future = event_loop.create_connection(
+        SMTPProtocol, host=hostname, port=server_port
+    )
+
+    _, protocol = await asyncio.wait_for(connect_future, timeout=1.0)
+
+    monkeypatch.setattr("aiosmtplib.protocol.MAX_RESPONSE_LENGTH", 128)
+
+    with pytest.raises(SMTPResponseException) as exc_info:
+        await protocol.execute_command(b"TEST", timeout=1.0)  # type: ignore
+
+    assert exc_info.value.code == -1
+    assert "Response too long" in exc_info.value.message
+
+    server.close()
+    await cleanup_server(server)
+
+
+async def test_protocol_response_continuation_overrun(
+    bind_address: str,
+    hostname: str,
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    event_loop = asyncio.get_running_loop()
+
+    async def client_connected(
+        reader: asyncio.StreamReader, writer: asyncio.StreamWriter
+    ) -> None:
+        await reader.read(1000)
+        # Endless multiline continuation; each line is well under the per-line
+        # cap, so only the total response cap can stop it.
+        writer.write(b"250-spam\r\n" * 100)
+        await writer.drain()
+
+    server = await asyncio.start_server(
+        client_connected, host=bind_address, port=0, family=socket.AF_INET
+    )
+    server_port = server.sockets[0].getsockname()[1] if server.sockets else 0
+    connect_future = event_loop.create_connection(
+        SMTPProtocol, host=hostname, port=server_port
+    )
+
+    _, protocol = await asyncio.wait_for(connect_future, timeout=1.0)
+
+    monkeypatch.setattr("aiosmtplib.protocol.MAX_RESPONSE_LENGTH", 128)
+
+    with pytest.raises(SMTPResponseException) as exc_info:
+        await protocol.execute_command(b"TEST", timeout=1.0)  # type: ignore
+
+    assert exc_info.value.code == -1
     assert "Response too long" in exc_info.value.message
 
     server.close()
@@ -162,6 +237,53 @@
     transport.close()
 
 
+async def test_protocol_discards_buffer_before_tls_handshake(
+    bind_address: str,
+    hostname: str,
+    client_tls_context: ssl.SSLContext,
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    """
+    Bytes a MITM injects after the 220 STARTTLS reply must not survive into the
+    encrypted session.
+    """
+    event_loop = asyncio.get_running_loop()
+
+    async def client_connected(
+        reader: asyncio.StreamReader, writer: asyncio.StreamWriter
+    ) -> None:
+        await reader.readuntil(b"\r\n")
+        # 220 reply plus injected plaintext, in a single segment.
+        writer.write(b"220 Go ahead\r\n250-mx.evil\r\n250 AUTH LOGIN\r\n")
+        await writer.drain()
+        await reader.read()  # keep the connection open through start_tls
+
+    server = await asyncio.start_server(
+        client_connected, host=bind_address, port=0, family=socket.AF_INET
+    )
+    server_port = server.sockets[0].getsockname()[1] if server.sockets else 0
+    connect_future = event_loop.create_connection(
+        SMTPProtocol, host=hostname, port=server_port
+    )
+    _, protocol = await asyncio.wait_for(connect_future, timeout=1.0)
+
+    captured: dict[str, bytes] = {}
+
+    async def mock_start_tls(transport, proto, *args, **kwargs):  # type: 
ignore[no-untyped-def]
+        captured["buffer"] = bytes(proto._buffer)
+        return transport
+
+    monkeypatch.setattr(event_loop, "start_tls", mock_start_tls)
+
+    response = await protocol.start_tls(client_tls_context, timeout=1.0)  # 
type: ignore[union-attr]
+
+    assert response.code == 220
+    assert captured["buffer"] == b""
+
+    server.close()
+    await cleanup_server(server)
+
+
 async def test_error_on_readline_with_partial_line(
     bind_address: str, hostname: str
 ) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_response.py 
new/aiosmtplib-5.1.2/tests/test_response.py
--- old/aiosmtplib-5.1.1/tests/test_response.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/tests/test_response.py 2020-02-02 01:00:00.000000000 
+0100
@@ -7,7 +7,11 @@
 @given(integers(), text())
 def test_response_repr(code: int, message: str) -> None:
     response = SMTPResponse(code, message)
-    assert repr(response) == f"({response.code}, {response.message})"
+    assert (
+        repr(response)
+        == f"SMTPResponse(code={response.code!r}, 
message={response.message!r})"
+    )
+    assert eval(repr(response)) == response
 
 
 @given(integers(), text())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiosmtplib-5.1.1/tests/test_timeouts.py 
new/aiosmtplib-5.1.2/tests/test_timeouts.py
--- old/aiosmtplib-5.1.1/tests/test_timeouts.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/aiosmtplib-5.1.2/tests/test_timeouts.py 2020-02-02 01:00:00.000000000 
+0100
@@ -27,6 +27,24 @@
     with pytest.raises(SMTPTimeoutError):
         await smtp_client.ehlo(hostname="example.com", timeout=0.0)
 
+    # A read timeout closes the connection, since the protocol is now desynced.
+    assert smtp_client.protocol is None
+    assert smtp_client.transport is None
+    assert not smtp_client.is_connected
+
+
[email protected]_mocks(smtp_EHLO=mock_response_delayed_ok)
+async def test_command_timeout_closes_connection(smtp_client: SMTP) -> None:
+    await smtp_client.connect()
+
+    with pytest.raises(SMTPTimeoutError):
+        await smtp_client.ehlo(hostname="example.com", timeout=0.0)
+
+    # Reusing a timed-out connection must fail cleanly rather than mispair a
+    # late server response with the next command.
+    with pytest.raises(SMTPServerDisconnected):
+        await smtp_client.noop()
+
 
 @pytest.mark.smtpd_mocks(smtp_DATA=mock_response_delayed_ok)
 async def test_data_timeout_error(smtp_client: SMTP) -> None:
@@ -37,6 +55,10 @@
     with pytest.raises(SMTPTimeoutError):
         await smtp_client.data("HELLO WORLD", timeout=0.0)
 
+    assert smtp_client.protocol is None
+    assert smtp_client.transport is None
+    assert not smtp_client.is_connected
+
 
 @pytest.mark.smtpd_mocks(_handle_client=mock_response_delayed_ok)
 async def test_timeout_error_on_connect(smtp_client: SMTP) -> None:
@@ -62,6 +84,10 @@
     with pytest.raises(SMTPTimeoutError):
         await smtp_client.starttls(timeout=0.0)
 
+    assert smtp_client.protocol is None
+    assert smtp_client.transport is None
+    assert not smtp_client.is_connected
+
 
 async def test_protocol_read_response_with_timeout_times_out(
     echo_server: asyncio.AbstractServer,

Reply via email to