diff --git a/CHANGELOG.md b/CHANGELOG.md index 876f17d..ff6fe58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Test connection always showed success; add per-account test endpoint ([`441e8b5`](https://github.com/christianlouis/InboxConverge/commit/441e8b54e958686fe52414393d715a18dfcda77c)) +- Fixed 21 mypy type errors across `notification_service.py`, `mail_processor.py`, `auth.py`, `tasks.py`, `providers.py`, `mail_accounts.py`, and `main.py` ## v0.3.0 (2026-03-28) diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index d60ace9..a6420aa 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -153,13 +153,13 @@ async def login( # Domain restriction — superusers always bypass if not user.is_superuser: - _check_domain_allowed(user.email) + _check_domain_allowed(str(user.email)) # Update last login user.last_login_at = datetime.now(timezone.utc) # type: ignore[assignment] # Auto-promote to superuser if this is the configured admin email - if not user.is_superuser and _is_admin_email(user.email): + if not user.is_superuser and _is_admin_email(str(user.email)): user.is_superuser = True # type: ignore[assignment] logger.info(f"Auto-promoted admin user: {user.email}") diff --git a/backend/app/api/v1/endpoints/mail_accounts.py b/backend/app/api/v1/endpoints/mail_accounts.py index 4e73ba7..3d870f4 100644 --- a/backend/app/api/v1/endpoints/mail_accounts.py +++ b/backend/app/api/v1/endpoints/mail_accounts.py @@ -75,7 +75,7 @@ async def create_mail_account( "pro": settings.TIER_PRO_MAX_ACCOUNTS, "enterprise": settings.TIER_ENTERPRISE_MAX_ACCOUNTS, } - max_accounts = tier_limits.get(current_user.subscription_tier.value, 1) + max_accounts = tier_limits.get(current_user.subscription_tier.value, 1) # type: ignore[assignment] if len(existing_accounts) >= max_accounts: raise HTTPException( @@ -320,7 +320,7 @@ async def test_existing_mail_connection( ) try: - password = decrypt_credential(account.encrypted_password) + password = decrypt_credential(str(account.encrypted_password)) except Exception: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/backend/app/api/v1/endpoints/providers.py b/backend/app/api/v1/endpoints/providers.py index 1757b87..5d814db 100644 --- a/backend/app/api/v1/endpoints/providers.py +++ b/backend/app/api/v1/endpoints/providers.py @@ -266,7 +266,7 @@ async def save_gmail_credential( gmail_email=credential_in.gmail_email, encrypted_access_token=encrypted_access, encrypted_refresh_token=encrypted_refresh, - scopes=build_gmail_credential_scopes(), + scopes=build_gmail_credential_scopes(granted_scopes=[]), is_valid=True, last_verified_at=datetime.now(timezone.utc), ) diff --git a/backend/app/main.py b/backend/app/main.py index 7c689b0..2469c9a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) @asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncIterator[None]: +async def lifespan(_app: FastAPI) -> AsyncIterator[None]: """Application lifespan handler for startup and shutdown events.""" # Startup logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}") diff --git a/backend/app/services/mail_processor.py b/backend/app/services/mail_processor.py index 208908f..523d586 100644 --- a/backend/app/services/mail_processor.py +++ b/backend/app/services/mail_processor.py @@ -183,18 +183,18 @@ class MailProcessor: if self.account.protocol == MailProtocol.POP3_SSL: context = ssl.create_default_context() pop_conn = poplib.POP3_SSL( - self.account.host, - self.account.port, + str(self.account.host), + int(self.account.port), context=context, timeout=30, ) else: - pop_conn = poplib.POP3( - self.account.host, self.account.port, timeout=30 + pop_conn = poplib.POP3( # type: ignore[assignment] + str(self.account.host), int(self.account.port), timeout=30 ) # Authenticate - pop_conn.user(self.account.username) + pop_conn.user(str(self.account.username)) pop_conn.pass_(self.password) # Retrieve UIDL map: {msg_number: uid_string} diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py index 5927199..df03532 100644 --- a/backend/app/services/notification_service.py +++ b/backend/app/services/notification_service.py @@ -60,7 +60,7 @@ async def send_user_notification( continue try: - success = await _send_apprise(config.apprise_url or "", title, body) + success = await _send_apprise(str(config.apprise_url or ""), title, body) if success: sent += 1 except Exception as exc: @@ -99,7 +99,7 @@ async def send_admin_notification( sent = 0 for config in configs: try: - success = await _send_apprise(config.apprise_url or "", title, body) + success = await _send_apprise(str(config.apprise_url or ""), title, body) if success: sent += 1 except Exception as exc: diff --git a/backend/app/workers/tasks.py b/backend/app/workers/tasks.py index f0f6c7c..d07eb31 100644 --- a/backend/app/workers/tasks.py +++ b/backend/app/workers/tasks.py @@ -137,6 +137,7 @@ async def process_mail_account(account_id: int): gmail_service = None smtp_config = None + gmail_cred = None if use_gmail_api: # Get user's Gmail credentials @@ -238,7 +239,7 @@ async def process_mail_account(account_id: int): error_msg: str | None = None try: - if use_gmail_api and gmail_service: + if use_gmail_api and gmail_service and gmail_cred: # Inject via Gmail API (preferred) label_ids = await gmail_service.build_import_label_ids( import_label_templates=gmail_cred.import_label_templates, @@ -286,7 +287,7 @@ async def process_mail_account(account_id: int): try: await send_user_notification( db=db, - user_id=account.user_id, + user_id=int(account.user_id), title="InboxRescue: Gmail Authorization Expired", body=f"Your Gmail credentials for account '{account.name}' have been revoked. Please re-authorize Gmail access in Settings.", notify_on_error=True, @@ -347,7 +348,7 @@ async def process_mail_account(account_id: int): run.emails_failed = emails_failed # type: ignore[assignment] run.completed_at = datetime.now(timezone.utc) # type: ignore[assignment] run.duration_seconds = ( - run.completed_at - _as_utc(run.started_at) + run.completed_at - _as_utc(run.started_at) # type: ignore[arg-type] ).total_seconds() run.status = "completed" if emails_failed == 0 else "partial_failure" # type: ignore[assignment] @@ -366,7 +367,7 @@ async def process_mail_account(account_id: int): try: await send_user_notification( db=db, - user_id=account.user_id, + user_id=int(account.user_id), title="InboxRescue: Mail Forwarding Failures", body=f"Mail account '{account.name}': {emails_failed} email(s) failed to forward.", notify_on_error=True, @@ -440,7 +441,7 @@ async def process_mail_account(account_id: int): try: await send_user_notification( db=db, - user_id=account.user_id, + user_id=int(account.user_id), title="InboxRescue: Mail Processing Error", body=f"Error processing mail account '{account.name}': {e}", notify_on_error=True, @@ -488,7 +489,7 @@ async def process_all_enabled_accounts(): ) stale_run.completed_at = datetime.now(timezone.utc) # type: ignore[assignment] stale_run.duration_seconds = ( # type: ignore[assignment] - stale_run.completed_at - _as_utc(stale_run.started_at) + stale_run.completed_at - _as_utc(stale_run.started_at) # type: ignore[arg-type] ).total_seconds() if stale_runs: await db.commit() @@ -571,12 +572,10 @@ async def cleanup_old_logs(days_to_keep: int = 30): stale_runs = stale_result.scalars().all() for stale_run in stale_runs: stale_run.status = "failed" # type: ignore[assignment] - stale_run.error_message = ( # type: ignore[assignment] - "Run timed out or worker was killed before completion" - ) + stale_run.error_message = "Run timed out or worker was killed before completion" # type: ignore[assignment] stale_run.completed_at = datetime.now(timezone.utc) # type: ignore[assignment] stale_run.duration_seconds = ( # type: ignore[assignment] - stale_run.completed_at - _as_utc(stale_run.started_at) + stale_run.completed_at - _as_utc(stale_run.started_at) # type: ignore[arg-type] ).total_seconds() if stale_runs: diff --git a/docs/TODO.md b/docs/TODO.md index ad3c077..82bca22 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,6 +5,7 @@ Comprehensive task breakdown for repository improvements and production readines ## ✅ Recently Completed - [x] **Pull Now**: Added "Pull Now" button on Accounts page that immediately queues a `process_mail_account` Celery task via `POST /mail-accounts/{id}/pull-now`. Button shows spinner while in flight and is disabled for inactive accounts. +- [x] Fixed 21 mypy type errors: `Column[T]` vs native type mismatches in `notification_service.py`, `mail_processor.py`, `auth.py`, `tasks.py`, `providers.py`, `mail_accounts.py`, and `main.py` (`lifespan` parameter rename). - [x] **Provider logos rework**: Logos now displayed as full-width banner strips at the top of each account card using `next/image fill + object-contain`. Handles all aspect ratios (1:1 square to 6:1 wordmark) without distortion. Proton Mail added. - [x] **Proton Mail provider**: Added Proton Mail preset in backend and ProviderWizard frontend. Domains: proton.me, protonmail.com, protonmail.ch, pm.me. Auto-detect and IMAP/POP3 Bridge settings included. - [x] Redesigned user-facing Logs page to mailbox-centric "Mailbox Activity" view: shows last check status per account + only successful pulls, suppressing noise from empty polling cycles.