Merge pull request #22 from christianlouis/copilot/fix-unused-imports
Fix all CI lint failures: ruff, mypy, and eslint
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
# Copilot Instructions
|
||||||
|
|
||||||
|
## Linting Requirements
|
||||||
|
|
||||||
|
Before committing any changes, always run the relevant linters and fix all errors:
|
||||||
|
|
||||||
|
### Backend (Python)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check code formatting
|
||||||
|
black --check backend/
|
||||||
|
|
||||||
|
# Lint with ruff
|
||||||
|
ruff check backend/
|
||||||
|
|
||||||
|
# Type check with mypy
|
||||||
|
mypy backend/app --ignore-missing-imports
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (TypeScript/React)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Python**: Follow PEP 8. Use `black` for formatting. All ruff and mypy errors must be resolved before committing.
|
||||||
|
- **TypeScript**: Follow the ESLint configuration. Avoid `any` types — use `unknown` with type narrowing instead. Remove unused variables and imports.
|
||||||
|
- **SQLAlchemy**: Use `# type: ignore[assignment]` for Column attribute assignments and `# noqa: E712` for `== True` comparisons in SQLAlchemy queries (these are valid ORM patterns).
|
||||||
@@ -11,7 +11,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
from app.models import database_models # Import all models
|
from app.models import database_models # noqa: F401 - Import all models for Alembic
|
||||||
|
|
||||||
# this is the Alembic Config object
|
# this is the Alembic Config object
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async def login(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Verify password
|
# Verify password
|
||||||
if not verify_password(form_data.password, user.hashed_password):
|
if not verify_password(form_data.password, user.hashed_password): # type: ignore[arg-type]
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Incorrect email or password",
|
detail="Incorrect email or password",
|
||||||
@@ -86,7 +86,7 @@ async def login(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update last login
|
# Update last login
|
||||||
user.last_login_at = datetime.utcnow()
|
user.last_login_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Create tokens
|
# Create tokens
|
||||||
@@ -129,11 +129,11 @@ async def google_oauth(
|
|||||||
if user:
|
if user:
|
||||||
# Update Google ID if not set
|
# Update Google ID if not set
|
||||||
if not user.google_id:
|
if not user.google_id:
|
||||||
user.google_id = google_id
|
user.google_id = google_id # type: ignore[assignment]
|
||||||
user.oauth_provider = "google"
|
user.oauth_provider = "google" # type: ignore[assignment]
|
||||||
|
|
||||||
# Update last login
|
# Update last login
|
||||||
user.last_login_at = datetime.utcnow()
|
user.last_login_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
|
|
||||||
logger.info(f"Existing user logged in with Google: {user.email}")
|
logger.info(f"Existing user logged in with Google: {user.email}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from sqlalchemy import select, desc
|
|||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.deps import get_current_active_user
|
from app.core.deps import get_current_active_user
|
||||||
from app.core.security import encrypt_credential, decrypt_credential
|
from app.core.security import encrypt_credential
|
||||||
from app.models.database_models import User, MailAccount
|
from app.models.database_models import User, MailAccount
|
||||||
from app.models.schemas import (
|
from app.models.schemas import (
|
||||||
MailAccountCreate,
|
MailAccountCreate,
|
||||||
@@ -52,7 +52,7 @@ async def create_mail_account(
|
|||||||
if len(existing_accounts) >= max_accounts:
|
if len(existing_accounts) >= max_accounts:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||||
detail=f"Account limit reached. Upgrade your subscription to add more accounts.",
|
detail="Account limit reached. Upgrade your subscription to add more accounts.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Encrypt password
|
# Encrypt password
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Notification configuration endpoints"""
|
"""Notification configuration endpoints"""
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ from app.models.database_models import User, NotificationConfig
|
|||||||
from app.models.schemas import (
|
from app.models.schemas import (
|
||||||
NotificationConfigCreate,
|
NotificationConfigCreate,
|
||||||
NotificationConfigResponse,
|
NotificationConfigResponse,
|
||||||
NotificationConfigUpdate,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.deps import get_current_active_user
|
from app.core.deps import get_current_active_user
|
||||||
from app.core.security import encrypt_credential, decrypt_credential
|
from app.core.security import encrypt_credential
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.models.database_models import User, GmailCredential
|
from app.models.database_models import User, GmailCredential
|
||||||
from app.models.schemas import (
|
from app.models.schemas import (
|
||||||
@@ -201,11 +201,11 @@ async def save_gmail_credential(
|
|||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Update existing
|
# Update existing
|
||||||
existing.gmail_email = credential_in.gmail_email
|
existing.gmail_email = credential_in.gmail_email # type: ignore[assignment]
|
||||||
existing.encrypted_access_token = encrypted_access
|
existing.encrypted_access_token = encrypted_access # type: ignore[assignment]
|
||||||
existing.encrypted_refresh_token = encrypted_refresh
|
existing.encrypted_refresh_token = encrypted_refresh # type: ignore[assignment]
|
||||||
existing.is_valid = True
|
existing.is_valid = True # type: ignore[assignment]
|
||||||
existing.last_verified_at = datetime.utcnow()
|
existing.last_verified_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(existing)
|
await db.refresh(existing)
|
||||||
return existing
|
return existing
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Subscription and payment endpoints"""
|
"""Subscription and payment endpoints"""
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ router = APIRouter()
|
|||||||
async def list_subscription_plans(db: AsyncSession = Depends(get_db)):
|
async def list_subscription_plans(db: AsyncSession = Depends(get_db)):
|
||||||
"""List all available subscription plans"""
|
"""List all available subscription plans"""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(SubscriptionPlan).where(SubscriptionPlan.is_active == True)
|
select(SubscriptionPlan).where(SubscriptionPlan.is_active == True) # noqa: E712
|
||||||
)
|
)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""User management endpoints"""
|
"""User management endpoints"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.deps import get_current_active_user
|
from app.core.deps import get_current_active_user
|
||||||
from app.models.database_models import User
|
from app.models.database_models import User
|
||||||
from app.models.schemas import UserResponse, UserDetailResponse, UserUpdate
|
from app.models.schemas import UserDetailResponse, UserUpdate
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ async def update_current_user_profile(
|
|||||||
):
|
):
|
||||||
"""Update current user profile"""
|
"""Update current user profile"""
|
||||||
if user_update.email:
|
if user_update.email:
|
||||||
current_user.email = user_update.email
|
current_user.email = user_update.email # type: ignore[assignment]
|
||||||
if user_update.full_name:
|
if user_update.full_name:
|
||||||
current_user.full_name = user_update.full_name
|
current_user.full_name = user_update.full_name # type: ignore[assignment]
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(current_user)
|
await db.refresh(current_user)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Supports environment variables and .env files.
|
|||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from pydantic import PostgresDsn, field_validator, ValidationInfo
|
from pydantic import field_validator
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
Database configuration and session management.
|
Database configuration and session management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
from sqlalchemy.orm import declarative_base
|
from sqlalchemy.orm import declarative_base
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -28,7 +30,7 @@ async_session_maker = async_sessionmaker(
|
|||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
async def get_db() -> AsyncSession:
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
"""Dependency for getting async database session"""
|
"""Dependency for getting async database session"""
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from app.core.security import decode_token
|
|||||||
from app.models.database_models import User
|
from app.models.database_models import User
|
||||||
|
|
||||||
# OAuth2 scheme for token authentication
|
# OAuth2 scheme for token authentication
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/api/v1/auth/login", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)
|
||||||
http_bearer = HTTPBearer(auto_error=False)
|
http_bearer = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class CSRFProtectionMiddleware(BaseHTTPMiddleware):
|
|||||||
For API-only applications, this is less critical but still good practice.
|
For API-only applications, this is less critical but still good practice.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app: ASGIApp, exempt_paths: list = None):
|
def __init__(self, app: ASGIApp, exempt_paths: list | None = None):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
self.exempt_paths = exempt_paths or [
|
self.exempt_paths = exempt_paths or [
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
|
|||||||
+1
-3
@@ -4,8 +4,6 @@ Main FastAPI application.
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -68,7 +66,7 @@ def create_application() -> FastAPI:
|
|||||||
"""Run on application startup"""
|
"""Run on application startup"""
|
||||||
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}")
|
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}")
|
||||||
logger.info(f"Debug mode: {settings.DEBUG}")
|
logger.info(f"Debug mode: {settings.DEBUG}")
|
||||||
logger.info(f"API documentation: /api/docs")
|
logger.info("API documentation: /api/docs")
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ Database models for the multi-tenant POP3 forwarder application.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Column,
|
Column,
|
||||||
Integer,
|
Integer,
|
||||||
@@ -86,7 +85,9 @@ class User(Base):
|
|||||||
oauth_provider = Column(String(50), nullable=True)
|
oauth_provider = Column(String(50), nullable=True)
|
||||||
|
|
||||||
# Subscription
|
# Subscription
|
||||||
subscription_tier = Column(SQLEnum(SubscriptionTier), default=SubscriptionTier.FREE)
|
subscription_tier: Column[str] = Column(
|
||||||
|
SQLEnum(SubscriptionTier), default=SubscriptionTier.FREE
|
||||||
|
)
|
||||||
subscription_status = Column(
|
subscription_status = Column(
|
||||||
String(50), default="active"
|
String(50), default="active"
|
||||||
) # active, canceled, past_due
|
) # active, canceled, past_due
|
||||||
@@ -128,7 +129,7 @@ class MailAccount(Base):
|
|||||||
email_address = Column(String(255), nullable=False)
|
email_address = Column(String(255), nullable=False)
|
||||||
|
|
||||||
# Server configuration
|
# Server configuration
|
||||||
protocol = Column(SQLEnum(MailProtocol), default=MailProtocol.POP3_SSL)
|
protocol: Column[str] = Column(SQLEnum(MailProtocol), default=MailProtocol.POP3_SSL)
|
||||||
host = Column(String(255), nullable=False)
|
host = Column(String(255), nullable=False)
|
||||||
port = Column(Integer, nullable=False)
|
port = Column(Integer, nullable=False)
|
||||||
use_ssl = Column(Boolean, default=True)
|
use_ssl = Column(Boolean, default=True)
|
||||||
@@ -142,10 +143,12 @@ class MailAccount(Base):
|
|||||||
forward_to = Column(String(255), nullable=False)
|
forward_to = Column(String(255), nullable=False)
|
||||||
|
|
||||||
# Delivery method
|
# Delivery method
|
||||||
delivery_method = Column(SQLEnum(DeliveryMethod), default=DeliveryMethod.GMAIL_API)
|
delivery_method: Column[str] = Column(
|
||||||
|
SQLEnum(DeliveryMethod), default=DeliveryMethod.GMAIL_API
|
||||||
|
)
|
||||||
|
|
||||||
# Status and settings
|
# Status and settings
|
||||||
status = Column(SQLEnum(AccountStatus), default=AccountStatus.ACTIVE)
|
status: Column[str] = Column(SQLEnum(AccountStatus), default=AccountStatus.ACTIVE)
|
||||||
is_enabled = Column(Boolean, default=True)
|
is_enabled = Column(Boolean, default=True)
|
||||||
check_interval_minutes = Column(Integer, default=5)
|
check_interval_minutes = Column(Integer, default=5)
|
||||||
max_emails_per_check = Column(Integer, default=50)
|
max_emails_per_check = Column(Integer, default=50)
|
||||||
@@ -264,7 +267,7 @@ class NotificationConfig(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Channel details
|
# Channel details
|
||||||
channel = Column(SQLEnum(NotificationChannel), nullable=False)
|
channel: Column[str] = Column(SQLEnum(NotificationChannel), nullable=False)
|
||||||
is_enabled = Column(Boolean, default=True)
|
is_enabled = Column(Boolean, default=True)
|
||||||
|
|
||||||
# Channel-specific configuration (stored as JSON)
|
# Channel-specific configuration (stored as JSON)
|
||||||
@@ -330,7 +333,7 @@ class SubscriptionPlan(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
# Plan details
|
# Plan details
|
||||||
tier = Column(SQLEnum(SubscriptionTier), unique=True, nullable=False)
|
tier: Column[str] = Column(SQLEnum(SubscriptionTier), unique=True, nullable=False)
|
||||||
name = Column(String(100), nullable=False)
|
name = Column(String(100), nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Pydantic schemas for API request/response validation.
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
from pydantic import BaseModel, EmailStr, Field, validator
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
OAuth2 authentication service for Google and other providers.
|
OAuth2 authentication service for Google and other providers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any
|
||||||
from datetime import datetime
|
|
||||||
import httpx
|
import httpx
|
||||||
from authlib.integrations.starlette_client import OAuth
|
from authlib.integrations.starlette_client import OAuth
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ from email.mime.text import MIMEText
|
|||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.utils import formatdate, make_msgid
|
from email.utils import formatdate, make_msgid
|
||||||
from typing import List, Dict, Any, Optional, Tuple
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from aioimaplib import aioimaplib
|
from aioimaplib import aioimaplib
|
||||||
|
|
||||||
from app.models.database_models import MailAccount, MailProtocol
|
from app.models.database_models import MailAccount, MailProtocol
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -150,12 +148,12 @@ class MailProcessor:
|
|||||||
Fetch emails from the mail server.
|
Fetch emails from the mail server.
|
||||||
Returns list of raw email data.
|
Returns list of raw email data.
|
||||||
"""
|
"""
|
||||||
max_count = max_count or self.account.max_emails_per_check
|
effective_max: int = max_count if max_count is not None else self.account.max_emails_per_check # type: ignore[assignment]
|
||||||
|
|
||||||
if self.account.protocol in [MailProtocol.POP3, MailProtocol.POP3_SSL]:
|
if self.account.protocol in [MailProtocol.POP3, MailProtocol.POP3_SSL]:
|
||||||
return await self._fetch_pop3_emails(max_count)
|
return await self._fetch_pop3_emails(effective_max)
|
||||||
else:
|
else:
|
||||||
return await self._fetch_imap_emails(max_count)
|
return await self._fetch_imap_emails(effective_max)
|
||||||
|
|
||||||
async def _fetch_pop3_emails(self, max_count: int) -> List[bytes]:
|
async def _fetch_pop3_emails(self, max_count: int) -> List[bytes]:
|
||||||
"""Fetch emails via POP3"""
|
"""Fetch emails via POP3"""
|
||||||
@@ -396,7 +394,7 @@ class MailServerAutoDetect:
|
|||||||
"""Auto-detect mail server settings based on email domain"""
|
"""Auto-detect mail server settings based on email domain"""
|
||||||
|
|
||||||
# Common mail server configurations
|
# Common mail server configurations
|
||||||
KNOWN_PROVIDERS = {
|
KNOWN_PROVIDERS: Dict[str, Dict[str, Any]] = {
|
||||||
"gmail.com": {
|
"gmail.com": {
|
||||||
"name": "Gmail",
|
"name": "Gmail",
|
||||||
"pop3_ssl": {"host": "pop.gmail.com", "port": 995},
|
"pop3_ssl": {"host": "pop.gmail.com", "port": 995},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Celery tasks for background email processing.
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ from app.services.mail_processor import MailProcessor
|
|||||||
from app.services.gmail_service import GmailService, GmailInjectionError
|
from app.services.gmail_service import GmailService, GmailInjectionError
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from sqlalchemy import select, and_
|
from sqlalchemy import select, and_
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -69,15 +67,15 @@ async def process_mail_account(account_id: int):
|
|||||||
await db.refresh(run)
|
await db.refresh(run)
|
||||||
|
|
||||||
# Decrypt password
|
# Decrypt password
|
||||||
password = decrypt_credential(account.encrypted_password)
|
password = decrypt_credential(account.encrypted_password) # type: ignore[arg-type]
|
||||||
|
|
||||||
# Create processor
|
# Create processor
|
||||||
processor = MailProcessor(account, password)
|
processor = MailProcessor(account, password)
|
||||||
|
|
||||||
# Fetch emails
|
# Fetch emails
|
||||||
emails = await processor.fetch_emails(account.max_emails_per_check)
|
emails = await processor.fetch_emails(account.max_emails_per_check) # type: ignore[arg-type]
|
||||||
|
|
||||||
run.emails_fetched = len(emails)
|
run.emails_fetched = len(emails) # type: ignore[assignment]
|
||||||
|
|
||||||
# Forward emails
|
# Forward emails
|
||||||
emails_forwarded = 0
|
emails_forwarded = 0
|
||||||
@@ -94,15 +92,15 @@ async def process_mail_account(account_id: int):
|
|||||||
gmail_cred_result = await db.execute(
|
gmail_cred_result = await db.execute(
|
||||||
select(GmailCredential).where(
|
select(GmailCredential).where(
|
||||||
GmailCredential.user_id == account.user_id,
|
GmailCredential.user_id == account.user_id,
|
||||||
GmailCredential.is_valid == True,
|
GmailCredential.is_valid == True, # noqa: E712
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
gmail_cred = gmail_cred_result.scalar_one_or_none()
|
gmail_cred = gmail_cred_result.scalar_one_or_none()
|
||||||
|
|
||||||
if gmail_cred:
|
if gmail_cred:
|
||||||
access_token = decrypt_credential(gmail_cred.encrypted_access_token)
|
access_token = decrypt_credential(gmail_cred.encrypted_access_token) # type: ignore[arg-type]
|
||||||
refresh_token = (
|
refresh_token = (
|
||||||
decrypt_credential(gmail_cred.encrypted_refresh_token)
|
decrypt_credential(gmail_cred.encrypted_refresh_token) # type: ignore[arg-type]
|
||||||
if gmail_cred.encrypted_refresh_token
|
if gmail_cred.encrypted_refresh_token
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
@@ -117,7 +115,7 @@ async def process_mail_account(account_id: int):
|
|||||||
f"Gmail API credentials not found for user {account.user_id}, "
|
f"Gmail API credentials not found for user {account.user_id}, "
|
||||||
f"falling back to SMTP for account {account.id}"
|
f"falling back to SMTP for account {account.id}"
|
||||||
)
|
)
|
||||||
use_gmail_api = False
|
use_gmail_api = False # type: ignore[assignment]
|
||||||
|
|
||||||
if not use_gmail_api:
|
if not use_gmail_api:
|
||||||
# Fall back to SMTP
|
# Fall back to SMTP
|
||||||
@@ -133,8 +131,8 @@ async def process_mail_account(account_id: int):
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"SMTP credentials not configured for account {account.id}"
|
f"SMTP credentials not configured for account {account.id}"
|
||||||
)
|
)
|
||||||
run.status = "failed"
|
run.status = "failed" # type: ignore[assignment]
|
||||||
run.error_message = "No delivery method configured (SMTP credentials missing and Gmail API not set up)"
|
run.error_message = "No delivery method configured (SMTP credentials missing and Gmail API not set up)" # type: ignore[assignment]
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -145,13 +143,13 @@ async def process_mail_account(account_id: int):
|
|||||||
await gmail_service.inject_email(
|
await gmail_service.inject_email(
|
||||||
raw_email=email_data,
|
raw_email=email_data,
|
||||||
label_ids=["INBOX"],
|
label_ids=["INBOX"],
|
||||||
source_account_name=account.name,
|
source_account_name=account.name, # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
emails_forwarded += 1
|
emails_forwarded += 1
|
||||||
else:
|
else:
|
||||||
# Forward via SMTP (fallback)
|
# Forward via SMTP (fallback)
|
||||||
success = await MailProcessor.forward_email(
|
success = await MailProcessor.forward_email(
|
||||||
email_data, account.name, account.forward_to, smtp_config
|
email_data, account.name, account.forward_to, smtp_config # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
if success:
|
if success:
|
||||||
emails_forwarded += 1
|
emails_forwarded += 1
|
||||||
@@ -163,24 +161,24 @@ async def process_mail_account(account_id: int):
|
|||||||
emails_failed += 1
|
emails_failed += 1
|
||||||
|
|
||||||
# Update run
|
# Update run
|
||||||
run.emails_forwarded = emails_forwarded
|
run.emails_forwarded = emails_forwarded # type: ignore[assignment]
|
||||||
run.emails_failed = emails_failed
|
run.emails_failed = emails_failed # type: ignore[assignment]
|
||||||
run.completed_at = datetime.utcnow()
|
run.completed_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
run.duration_seconds = (run.completed_at - run.started_at).total_seconds()
|
run.duration_seconds = (run.completed_at - run.started_at).total_seconds()
|
||||||
run.status = "completed" if emails_failed == 0 else "partial_failure"
|
run.status = "completed" if emails_failed == 0 else "partial_failure" # type: ignore[assignment]
|
||||||
|
|
||||||
# Update account
|
# Update account
|
||||||
account.total_emails_processed += emails_forwarded
|
account.total_emails_processed += emails_forwarded # type: ignore[assignment]
|
||||||
account.total_emails_failed += emails_failed
|
account.total_emails_failed += emails_failed # type: ignore[assignment]
|
||||||
account.last_check_at = datetime.utcnow()
|
account.last_check_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
|
|
||||||
if emails_failed == 0:
|
if emails_failed == 0:
|
||||||
account.last_successful_check_at = datetime.utcnow()
|
account.last_successful_check_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
account.status = AccountStatus.ACTIVE
|
account.status = AccountStatus.ACTIVE # type: ignore[assignment]
|
||||||
else:
|
else:
|
||||||
account.status = AccountStatus.ERROR
|
account.status = AccountStatus.ERROR # type: ignore[assignment]
|
||||||
account.last_error_at = datetime.utcnow()
|
account.last_error_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
account.last_error_message = f"{emails_failed} emails failed to forward"
|
account.last_error_message = f"{emails_failed} emails failed to forward" # type: ignore[assignment]
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -194,18 +192,18 @@ async def process_mail_account(account_id: int):
|
|||||||
|
|
||||||
# Mark run as failed
|
# Mark run as failed
|
||||||
if "run" in locals():
|
if "run" in locals():
|
||||||
run.status = "failed"
|
run.status = "failed" # type: ignore[assignment]
|
||||||
run.error_message = str(e)
|
run.error_message = str(e) # type: ignore[assignment]
|
||||||
run.completed_at = datetime.utcnow()
|
run.completed_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
run.duration_seconds = (
|
run.duration_seconds = (
|
||||||
run.completed_at - run.started_at
|
run.completed_at - run.started_at
|
||||||
).total_seconds()
|
).total_seconds()
|
||||||
|
|
||||||
# Update account error status
|
# Update account error status
|
||||||
if "account" in locals():
|
if "account" in locals() and account is not None:
|
||||||
account.status = AccountStatus.ERROR
|
account.status = AccountStatus.ERROR # type: ignore[assignment]
|
||||||
account.last_error_at = datetime.utcnow()
|
account.last_error_at = datetime.utcnow() # type: ignore[assignment]
|
||||||
account.last_error_message = str(e)
|
account.last_error_message = str(e) # type: ignore[assignment]
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -222,7 +220,7 @@ async def process_all_enabled_accounts():
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(MailAccount).where(
|
select(MailAccount).where(
|
||||||
and_(
|
and_(
|
||||||
MailAccount.is_enabled == True,
|
MailAccount.is_enabled == True, # noqa: E712
|
||||||
MailAccount.status.in_(
|
MailAccount.status.in_(
|
||||||
[AccountStatus.ACTIVE, AccountStatus.TESTING]
|
[AccountStatus.ACTIVE, AccountStatus.TESTING]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ Test configuration and fixtures.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
from typing import AsyncGenerator
|
||||||
from typing import AsyncGenerator, Generator
|
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Unit tests for Gmail service module.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock, patch, AsyncMock
|
from unittest.mock import MagicMock
|
||||||
from app.services.gmail_service import GmailService, GmailInjectionError, GMAIL_SCOPES
|
from app.services.gmail_service import GmailService, GmailInjectionError, GMAIL_SCOPES
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
Unit tests for provider presets and mail server auto-detection.
|
Unit tests for provider presets and mail server auto-detection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
from app.services.mail_processor import MailServerAutoDetect
|
from app.services.mail_processor import MailServerAutoDetect
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useAuthStore } from '@/store/authStore';
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const setUser = useAuthStore((state) => state.setUser);
|
const _setUser = useAuthStore((state) => state.setUser);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -25,8 +25,9 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
// Redirect to dashboard
|
// Redirect to dashboard
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err.response?.data?.detail || 'Login failed. Please try again.');
|
const error = err as { response?: { data?: { detail?: string } } };
|
||||||
|
setError(error.response?.data?.detail || 'Login failed. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -37,7 +38,7 @@ export default function LoginPage() {
|
|||||||
const redirectUri = `${window.location.origin}/auth/callback`;
|
const redirectUri = `${window.location.origin}/auth/callback`;
|
||||||
const authUrl = await authApi.getGoogleAuthUrl(redirectUri);
|
const authUrl = await authApi.getGoogleAuthUrl(redirectUri);
|
||||||
window.location.href = authUrl;
|
window.location.href = authUrl;
|
||||||
} catch (err: any) {
|
} catch (_err: unknown) {
|
||||||
setError('Failed to initialize Google login');
|
setError('Failed to initialize Google login');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ export default function RegisterPage() {
|
|||||||
localStorage.setItem('access_token', loginResponse.access_token);
|
localStorage.setItem('access_token', loginResponse.access_token);
|
||||||
|
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err.response?.data?.detail || 'Registration failed. Please try again.');
|
const error = err as { response?: { data?: { detail?: string } } };
|
||||||
|
setError(error.response?.data?.detail || 'Registration failed. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user