diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb077a..0cc6737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Lint**: Removed unused imports (`pytest` in `test_dns_fallback.py`, `asyncio` in `test_mail_processor_pop3.py`); reformatted three test files with Black; resolved mypy type errors in `mail_processor.py` (IPv4 address tuple indexing, `_create_socket` override) and suppressed spurious `alembic.command` attr-defined error in `main.py`. + - **POP3/IMAP connectivity**: Added retry logic (up to 3 attempts, 5 s delay) for transient errors (`-ERR EOF`, timeout, connection-reset) in both POP3 and IMAP fetch paths. Most `-ERR EOF` and "timed out" failures now self-heal without diff --git a/backend/app/main.py b/backend/app/main.py index 2469c9a..be3f1be 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -13,7 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware from prometheus_client import generate_latest, CONTENT_TYPE_LATEST import logging from alembic.config import Config as AlembicConfig -from alembic import command as alembic_command +from alembic import command as alembic_command # type: ignore[attr-defined] from app.core.config import settings from app.core.middleware import SecurityHeadersMiddleware, CSRFProtectionMiddleware diff --git a/backend/app/services/mail_processor.py b/backend/app/services/mail_processor.py index 778a11b..b1f28bf 100644 --- a/backend/app/services/mail_processor.py +++ b/backend/app/services/mail_processor.py @@ -196,7 +196,7 @@ def _resolve_ipv4_sync(host: str, port: int) -> Optional[str]: try: infos = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM) if infos: - ipv4 = infos[0][4][0] + ipv4: str = infos[0][4][0] # type: ignore[assignment] if settings.DNS_CACHE_FALLBACK_ENABLED: _set_cached_ipv4(host, port, ipv4) return ipv4 @@ -250,10 +250,10 @@ class _POP3WithIPv4Pref(poplib.POP3): self._ipv4_addr = _ipv4_addr super().__init__(host, port, timeout) - def _create_socket(self, timeout: Any) -> socket.socket: # type: ignore[override] + def _create_socket(self, timeout: Any) -> socket.socket: # type: ignore[override,misc] if self._ipv4_addr: return socket.create_connection((self._ipv4_addr, self.port), timeout) - return super()._create_socket(timeout) + return super()._create_socket(timeout) # type: ignore[misc] class _POP3SSLWithIPv4Pref(poplib.POP3_SSL): @@ -279,10 +279,10 @@ class _POP3SSLWithIPv4Pref(poplib.POP3_SSL): self._ipv4_addr = _ipv4_addr super().__init__(host, port, timeout=timeout, context=context) - def _create_socket(self, timeout: Any) -> socket.socket: # type: ignore[override] + def _create_socket(self, timeout: Any) -> socket.socket: # type: ignore[override,misc] if self._ipv4_addr: return socket.create_connection((self._ipv4_addr, self.port), timeout) - return super()._create_socket(timeout) + return super()._create_socket(timeout) # type: ignore[misc] def _make_pop3_conn( diff --git a/backend/tests/unit/test_dns_fallback.py b/backend/tests/unit/test_dns_fallback.py index 06bacf2..5696b4c 100644 --- a/backend/tests/unit/test_dns_fallback.py +++ b/backend/tests/unit/test_dns_fallback.py @@ -13,7 +13,6 @@ import socket import struct from unittest.mock import MagicMock, patch -import pytest from app.services.mail_processor import ( _build_dns_query, @@ -24,7 +23,6 @@ from app.services.mail_processor import ( _dns_cache_lock, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/backend/tests/unit/test_mail_processor_pop3.py b/backend/tests/unit/test_mail_processor_pop3.py index da9566f..77b374b 100644 --- a/backend/tests/unit/test_mail_processor_pop3.py +++ b/backend/tests/unit/test_mail_processor_pop3.py @@ -869,7 +869,6 @@ class TestFetchPop3Retry: @pytest.mark.asyncio async def test_retries_on_eof_error(self, mock_poplib, _mock_resolve): """EOF error on first attempt triggers a retry; second attempt succeeds.""" - import asyncio import poplib as real_poplib # First call raises EOF; second succeeds @@ -910,9 +909,7 @@ class TestFetchPop3Retry: account = _make_account(protocol="pop3_ssl") proc = MailProcessor(account, "secret") - with patch( - "app.services.mail_processor.asyncio.sleep", new_callable=AsyncMock - ): + with patch("app.services.mail_processor.asyncio.sleep", new_callable=AsyncMock): with pytest.raises(MailFetchError): await proc._fetch_pop3_emails(10, set()) diff --git a/backend/tests/unit/test_tasks.py b/backend/tests/unit/test_tasks.py index f0d0ec2..e725741 100644 --- a/backend/tests/unit/test_tasks.py +++ b/backend/tests/unit/test_tasks.py @@ -1945,7 +1945,9 @@ class TestNotificationBackoff: account = _make_account(delivery_method=DeliveryMethod.SMTP, **overrides) account.debug_logging = False account.debug_logging_run_count = 0 - account.error_notification_sent = overrides.get("error_notification_sent", False) + account.error_notification_sent = overrides.get( + "error_notification_sent", False + ) account.status = MagicMock(value="active") return account @@ -1995,7 +1997,10 @@ class TestNotificationBackoff: mock_send_notification = AsyncMock(return_value=1) with ( - patch(f"{MODULE}.async_session_maker", side_effect=[maker(), session_maker_side_effect()]), + patch( + f"{MODULE}.async_session_maker", + side_effect=[maker(), session_maker_side_effect()], + ), patch(f"{MODULE}.engine", AsyncMock()), patch(f"{MODULE}.decrypt_credential", return_value="pw"), patch(f"{MODULE}.MailProcessor", return_value=mock_processor),