Files
gh-christianlouis-inboxconv…/backend/app/api/v1/endpoints/mail_accounts.py
T

277 lines
8.7 KiB
Python

"""Mail account management endpoints"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.core.database import get_db
from app.core.deps import get_current_active_user
from app.core.security import encrypt_credential
from app.models.database_models import (
User,
MailAccount,
AccountStatus,
SubscriptionPlan,
)
from app.models.schemas import (
MailAccountCreate,
MailAccountResponse,
MailAccountUpdate,
MailAccountTestRequest,
MailAccountTestResponse,
MailAccountAutoDetectRequest,
MailAccountAutoDetectResponse,
)
from app.services.mail_processor import MailProcessor, MailServerAutoDetect
from app.core.config import settings
router = APIRouter()
@router.post(
"", response_model=MailAccountResponse, status_code=status.HTTP_201_CREATED
)
async def create_mail_account(
account_in: MailAccountCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new mail account"""
# Superusers are not subject to subscription limits
if not current_user.is_superuser:
# Count existing accounts for this user
result = await db.execute(
select(MailAccount).where(MailAccount.user_id == current_user.id)
)
existing_accounts = result.scalars().all()
# Try to look up the limit from the active SubscriptionPlan in the DB first
# so that admin-managed plan limits take effect immediately.
plan_result = await db.execute(
select(SubscriptionPlan).where(
SubscriptionPlan.tier == current_user.subscription_tier,
SubscriptionPlan.is_active.is_(True),
)
)
plan = plan_result.scalar_one_or_none()
if plan is not None:
max_accounts = plan.max_mail_accounts
else:
# Fall back to env-var / config values when no plan row exists
tier_limits = {
"free": settings.TIER_FREE_MAX_ACCOUNTS,
"basic": settings.TIER_BASIC_MAX_ACCOUNTS,
"pro": settings.TIER_PRO_MAX_ACCOUNTS,
"enterprise": settings.TIER_ENTERPRISE_MAX_ACCOUNTS,
}
max_accounts = tier_limits.get(current_user.subscription_tier.value, 1)
if len(existing_accounts) >= max_accounts:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail="Account limit reached. Upgrade your subscription to add more accounts.",
)
# Encrypt password
encrypted_password = encrypt_credential(account_in.password)
# Create account
account = MailAccount(
user_id=current_user.id,
name=account_in.name,
email_address=account_in.email_address,
protocol=account_in.protocol,
host=account_in.host,
port=account_in.port,
use_ssl=account_in.use_ssl,
use_tls=account_in.use_tls,
username=account_in.username,
encrypted_password=encrypted_password,
forward_to=account_in.forward_to,
delivery_method=account_in.delivery_method,
is_enabled=account_in.is_enabled,
check_interval_minutes=account_in.check_interval_minutes,
max_emails_per_check=account_in.max_emails_per_check,
delete_after_forward=account_in.delete_after_forward,
)
db.add(account)
await db.commit()
await db.refresh(account)
return account
@router.get("", response_model=List[MailAccountResponse])
async def list_mail_accounts(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""List all mail accounts for current user"""
result = await db.execute(
select(MailAccount)
.where(MailAccount.user_id == current_user.id)
.order_by(desc(MailAccount.created_at))
)
accounts = result.scalars().all()
return accounts
@router.get("/{account_id}", response_model=MailAccountResponse)
async def get_mail_account(
account_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Get a specific mail account"""
result = await db.execute(
select(MailAccount).where(
MailAccount.id == account_id, MailAccount.user_id == current_user.id
)
)
account = result.scalar_one_or_none()
if not account:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Mail account not found"
)
return account
@router.put("/{account_id}", response_model=MailAccountResponse)
async def update_mail_account(
account_id: int,
account_update: MailAccountUpdate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Update a mail account"""
result = await db.execute(
select(MailAccount).where(
MailAccount.id == account_id, MailAccount.user_id == current_user.id
)
)
account = result.scalar_one_or_none()
if not account:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Mail account not found"
)
# Update fields
update_data = account_update.model_dump(exclude_unset=True)
if "password" in update_data:
password = update_data.pop("password")
if password: # Only update when a non-empty password is provided
update_data["encrypted_password"] = encrypt_credential(password)
for field, value in update_data.items():
setattr(account, field, value)
await db.commit()
await db.refresh(account)
return account
@router.delete("/{account_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_mail_account(
account_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Delete a mail account"""
result = await db.execute(
select(MailAccount).where(
MailAccount.id == account_id, MailAccount.user_id == current_user.id
)
)
account = result.scalar_one_or_none()
if not account:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Mail account not found"
)
await db.delete(account)
await db.commit()
@router.patch("/{account_id}/toggle", response_model=MailAccountResponse)
async def toggle_mail_account(
account_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Toggle the enabled/disabled state of a mail account"""
result = await db.execute(
select(MailAccount).where(
MailAccount.id == account_id, MailAccount.user_id == current_user.id
)
)
account = result.scalar_one_or_none()
if not account:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Mail account not found"
)
account.is_enabled = not account.is_enabled # type: ignore[assignment]
# When re-enabling a previously errored account, reset status to ACTIVE
# so the scheduler picks it up on the next run.
if account.is_enabled and account.status == AccountStatus.ERROR:
account.status = AccountStatus.ACTIVE # type: ignore[assignment]
await db.commit()
await db.refresh(account)
return account
@router.post("/test", response_model=MailAccountTestResponse)
async def test_mail_connection(
test_request: MailAccountTestRequest,
current_user: User = Depends(get_current_active_user),
):
"""Test connection to mail server"""
# Create temporary account for testing
temp_account = MailAccount(
user_id=current_user.id,
name="test",
email_address="test@test.com",
protocol=test_request.protocol,
host=test_request.host,
port=test_request.port,
use_ssl=test_request.use_ssl,
use_tls=test_request.use_tls,
username=test_request.username,
encrypted_password="", # Not used for test
forward_to="test@test.com",
)
processor = MailProcessor(temp_account, test_request.password)
success, message = await processor.test_connection()
return MailAccountTestResponse(success=success, message=message)
@router.post("/auto-detect", response_model=MailAccountAutoDetectResponse)
async def auto_detect_mail_settings(
detect_request: MailAccountAutoDetectRequest,
current_user: User = Depends(get_current_active_user),
):
"""Auto-detect mail server settings for an email address"""
suggestions = MailServerAutoDetect.detect(detect_request.email_address)
return MailAccountAutoDetectResponse(
success=len(suggestions) > 0, suggestions=suggestions
)