From 63345dcbaa247c4bb91e916920f535b325a4f614 Mon Sep 17 00:00:00 2001 From: Fernando Celmer Date: Sun, 17 May 2026 18:35:38 -0300 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=AA=B2=20BUG-#69:=20Forward=20port/ss?= =?UTF-8?q?l=20overrides=20through=20auto-discovery=20factories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- email_profile/core/credentials.py | 32 +++++++++++++++++++++++++------ email_profile/email.py | 24 +++++++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/email_profile/core/credentials.py b/email_profile/core/credentials.py index d0f577e..cdb30d2 100644 --- a/email_profile/core/credentials.py +++ b/email_profile/core/credentials.py @@ -4,6 +4,7 @@ import os from dataclasses import dataclass +from typing import Optional from email_profile.providers import resolve_imap_host @@ -29,15 +30,25 @@ class EmailFactories: """Build :class:`Credentials` without the user spelling out a hostname.""" @classmethod - def from_address(cls, address: str, password: str) -> Credentials: - """Auto-discover the IMAP host from the email address.""" + def from_address( + cls, + address: str, + password: str, + *, + port: Optional[int] = None, + ssl: Optional[bool] = None, + ) -> Credentials: + """Auto-discover the IMAP host from the email address. + + ``port`` and ``ssl`` override the discovered values when given. + """ host = resolve_imap_host(address) return Credentials( server=host.host, user=address, password=password, - port=host.port, - ssl=host.ssl, + port=host.port if port is None else port, + ssl=host.ssl if ssl is None else ssl, ) @classmethod @@ -47,6 +58,9 @@ def from_env( user_var: str = "EMAIL_USERNAME", password_var: str = "EMAIL_PASSWORD", load_dotenv: bool = True, + *, + port: Optional[int] = None, + ssl: Optional[bool] = None, ) -> Credentials: """Read credentials from env vars (or `.env`).""" if load_dotenv: @@ -66,6 +80,12 @@ def from_env( server = os.environ.get(server_var) if server: - return Credentials(server=server, user=user, password=password) + return Credentials( + server=server, + user=user, + password=password, + port=993 if port is None else port, + ssl=True if ssl is None else ssl, + ) - return cls.from_address(user, password) + return cls.from_address(user, password, port=port, ssl=ssl) diff --git a/email_profile/email.py b/email_profile/email.py index a66eea2..8e93654 100644 --- a/email_profile/email.py +++ b/email_profile/email.py @@ -42,8 +42,8 @@ def __init__( server: Optional[str] = None, user: Optional[str] = None, password: Optional[str] = None, - port: int = 993, - ssl: bool = True, + port: Optional[int] = None, + ssl: Optional[bool] = None, storage: Optional[StorageABC] = None, ) -> None: connection = self._resolve(server, user, password, port, ssl) @@ -77,17 +77,21 @@ def _resolve( server: Optional[str], user: Optional[str], password: Optional[str], - port: int, - ssl: bool, + port: Optional[int], + ssl: Optional[bool], ) -> Credentials: if server is None and user is None and password is None: - return EmailFactories.from_env() + return EmailFactories.from_env(port=port, ssl=ssl) if password is None and user is not None and server and "@" in server: - return EmailFactories.from_address(server, user) + return EmailFactories.from_address( + server, user, port=port, ssl=ssl + ) if server is None and user is not None and "@" in user and password: - return EmailFactories.from_address(user, password) + return EmailFactories.from_address( + user, password, port=port, ssl=ssl + ) if server is None or user is None or password is None: raise TypeError( @@ -97,7 +101,11 @@ def _resolve( ) return Credentials( - server=server, user=user, password=password, port=port, ssl=ssl + server=server, + user=user, + password=password, + port=993 if port is None else port, + ssl=True if ssl is None else ssl, ) @property From 05c31145f5d97c65adf1eeda5055ac013e0d2b4d Mon Sep 17 00:00:00 2001 From: Fernando Celmer Date: Sun, 17 May 2026 18:35:45 -0300 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9D=A4=EF=B8=8F=20TEST-#69:=20Add=20cove?= =?UTF-8?q?rage=20for=20port/ssl=20override=20over=20auto-discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_email.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_email.py b/tests/test_email.py index 4b77c40..02eacd4 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -186,6 +186,31 @@ def test_is_connected_true_after_connect(self): self.assertFalse(app.is_connected) +class TestAutoDiscoveryOverrides(TestCase): + def test_kwargs_override_discovered_port_and_ssl(self): + from email_profile.core.types import IMAPHost + + with patch( + "email_profile.core.credentials.resolve_imap_host", + return_value=IMAPHost("imap.gmail.com", port=993, ssl=True), + ): + app = Email("u@gmail.com", "pw", port=2143, ssl=False) + self.assertEqual(app.port, 2143) + self.assertFalse(app.ssl) + self.assertEqual(app.server, "imap.gmail.com") + + def test_no_override_keeps_discovered_values(self): + from email_profile.core.types import IMAPHost + + with patch( + "email_profile.core.credentials.resolve_imap_host", + return_value=IMAPHost("imap.gmail.com", port=993, ssl=True), + ): + app = Email("u@gmail.com", "pw") + self.assertEqual(app.port, 993) + self.assertTrue(app.ssl) + + class TestConstructorOverloads(TestCase): def test_three_positional_args_explicit(self): app = Email("imap.x.com", "u", "pw")