Add Gmail API injection, provider presets, delivery method, and frontend wizard

- Add gmail_service.py with Gmail API users.messages.insert() for direct email injection
- Add DeliveryMethod enum and GmailCredential model to database models
- Add delivery_method field to MailAccount schema and model
- Create providers.py endpoint with 12 provider presets (Gmail, GMX, WEB.DE, Outlook, Yahoo, AOL, T-Online, 1&1/IONOS, Freenet, Posteo, mail.de, iCloud)
- Expand MailServerAutoDetect with 30+ domain mappings
- Update Celery tasks to prefer Gmail API injection, with SMTP fallback
- Add ProviderWizard.tsx frontend component for quick provider setup
- Update AddMailAccountModal.tsx with wizard integration
- Add Google API Python client libraries to requirements.txt
- Add Gmail API config settings
- Add unit tests for Gmail service and provider presets

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/de3ef930-a980-4958-8a9d-a2c802918e81
This commit is contained in:
copilot-swe-agent[bot]
2026-03-22 19:00:30 +00:00
parent 6435af3251
commit 40150da3a9
14 changed files with 1580 additions and 191 deletions
+2 -1
View File
@@ -3,7 +3,7 @@ API v1 router aggregation.
"""
from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, mail_accounts, notifications, subscriptions, admin
from app.api.v1.endpoints import auth, users, mail_accounts, notifications, subscriptions, admin, providers
api_router = APIRouter()
@@ -11,6 +11,7 @@ api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
api_router.include_router(users.router, prefix="/users", tags=["Users"])
api_router.include_router(mail_accounts.router, prefix="/mail-accounts", tags=["Mail Accounts"])
api_router.include_router(providers.router, prefix="/providers", tags=["Providers & Gmail"])
api_router.include_router(notifications.router, prefix="/notifications", tags=["Notifications"])
api_router.include_router(subscriptions.router, prefix="/subscriptions", tags=["Subscriptions"])
api_router.include_router(admin.router, prefix="/admin", tags=["Admin"])
@@ -64,6 +64,7 @@ async def create_mail_account(
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,
+260
View File
@@ -0,0 +1,260 @@
"""Provider presets and Gmail credential management endpoints"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.deps import get_current_active_user
from app.core.security import encrypt_credential, decrypt_credential
from app.core.config import settings
from app.models.database_models import User, GmailCredential
from app.models.schemas import (
ProviderPreset,
ProviderListResponse,
GmailCredentialCreate,
GmailCredentialResponse,
)
from app.services.gmail_service import GmailService
router = APIRouter()
# Provider presets with server configurations
PROVIDER_PRESETS: List[ProviderPreset] = [
ProviderPreset(
id="gmail",
name="Gmail",
icon="gmail",
domains=["gmail.com", "googlemail.com"],
imap_ssl={"host": "imap.gmail.com", "port": 993},
pop3_ssl={"host": "pop.gmail.com", "port": 995},
notes="Enable IMAP/POP3 in Gmail settings. Use an App Password if 2FA is enabled.",
),
ProviderPreset(
id="gmx",
name="GMX",
icon="gmx",
domains=["gmx.de", "gmx.net", "gmx.at", "gmx.ch", "gmx.com"],
imap_ssl={"host": "imap.gmx.net", "port": 993},
pop3_ssl={"host": "pop.gmx.net", "port": 995},
notes="Enable POP3/IMAP in GMX settings under E-Mail > POP3/IMAP Abruf.",
),
ProviderPreset(
id="webde",
name="WEB.DE",
icon="webde",
domains=["web.de"],
imap_ssl={"host": "imap.web.de", "port": 993},
pop3_ssl={"host": "pop3.web.de", "port": 995},
notes="Enable POP3/IMAP in WEB.DE settings under E-Mail > POP3/IMAP Abruf.",
),
ProviderPreset(
id="outlook",
name="Outlook / Hotmail",
icon="outlook",
domains=["outlook.com", "hotmail.com", "live.com", "msn.com", "outlook.de"],
imap_ssl={"host": "outlook.office365.com", "port": 993},
pop3_ssl={"host": "outlook.office365.com", "port": 995},
notes="Use your Microsoft account credentials.",
),
ProviderPreset(
id="yahoo",
name="Yahoo Mail",
icon="yahoo",
domains=["yahoo.com", "yahoo.de", "yahoo.co.uk", "ymail.com"],
imap_ssl={"host": "imap.mail.yahoo.com", "port": 993},
pop3_ssl={"host": "pop.mail.yahoo.com", "port": 995},
notes="Generate an App Password in Yahoo account security settings.",
),
ProviderPreset(
id="aol",
name="AOL Mail",
icon="aol",
domains=["aol.com", "aim.com"],
imap_ssl={"host": "imap.aol.com", "port": 993},
pop3_ssl={"host": "pop.aol.com", "port": 995},
notes="Generate an App Password in AOL account security settings.",
),
ProviderPreset(
id="tonline",
name="T-Online",
icon="tonline",
domains=["t-online.de"],
imap_ssl={"host": "secureimap.t-online.de", "port": 993},
pop3_ssl={"host": "securepop.t-online.de", "port": 995},
notes="Use your T-Online E-Mail-Passwort (not your Telekom login password).",
),
ProviderPreset(
id="ionos",
name="1&1 / IONOS",
icon="ionos",
domains=["online.de", "onlinehome.de", "1und1.de"],
imap_ssl={"host": "imap.ionos.de", "port": 993},
pop3_ssl={"host": "pop.ionos.de", "port": 995},
notes="Use your IONOS email credentials.",
),
ProviderPreset(
id="freenet",
name="Freenet",
icon="freenet",
domains=["freenet.de"],
imap_ssl={"host": "mx.freenet.de", "port": 993},
pop3_ssl={"host": "mx.freenet.de", "port": 995},
notes="Use your Freenet email credentials.",
),
ProviderPreset(
id="posteo",
name="Posteo",
icon="posteo",
domains=["posteo.de", "posteo.net"],
imap_ssl={"host": "posteo.de", "port": 993},
pop3_ssl=None,
notes="Posteo supports IMAP only. Use your Posteo credentials.",
),
ProviderPreset(
id="mailde",
name="mail.de",
icon="mailde",
domains=["mail.de"],
imap_ssl={"host": "imap.mail.de", "port": 993},
pop3_ssl={"host": "pop.mail.de", "port": 995},
notes="Use your mail.de email credentials.",
),
ProviderPreset(
id="icloud",
name="iCloud Mail",
icon="icloud",
domains=["icloud.com", "me.com", "mac.com"],
imap_ssl={"host": "imap.mail.me.com", "port": 993},
pop3_ssl=None,
notes="Generate an app-specific password at appleid.apple.com.",
),
]
@router.get("/presets", response_model=ProviderListResponse)
async def list_provider_presets(
current_user: User = Depends(get_current_active_user),
):
"""List all available mail provider presets for quick setup wizard"""
return ProviderListResponse(providers=PROVIDER_PRESETS)
@router.get("/presets/{provider_id}", response_model=ProviderPreset)
async def get_provider_preset(
provider_id: str,
current_user: User = Depends(get_current_active_user),
):
"""Get a specific provider preset by ID"""
for preset in PROVIDER_PRESETS:
if preset.id == provider_id:
return preset
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Provider '{provider_id}' not found",
)
@router.post("/gmail-credential", response_model=GmailCredentialResponse, status_code=status.HTTP_201_CREATED)
async def save_gmail_credential(
credential_in: GmailCredentialCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""
Save Gmail API OAuth2 credentials for the current user.
These are used to inject emails directly into Gmail via the API.
"""
# Verify the credentials work
gmail_service = GmailService(
access_token=credential_in.access_token,
refresh_token=credential_in.refresh_token,
client_id=settings.GOOGLE_CLIENT_ID,
client_secret=settings.GOOGLE_CLIENT_SECRET,
)
is_valid = await gmail_service.verify_access()
if not is_valid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Gmail API credentials are invalid or expired",
)
# Check for existing credential
result = await db.execute(
select(GmailCredential).where(GmailCredential.user_id == current_user.id)
)
existing = result.scalar_one_or_none()
encrypted_access = encrypt_credential(credential_in.access_token)
encrypted_refresh = (
encrypt_credential(credential_in.refresh_token) if credential_in.refresh_token else None
)
if existing:
# Update existing
existing.gmail_email = credential_in.gmail_email
existing.encrypted_access_token = encrypted_access
existing.encrypted_refresh_token = encrypted_refresh
existing.is_valid = True
from datetime import datetime
existing.last_verified_at = datetime.utcnow()
await db.commit()
await db.refresh(existing)
return existing
else:
# Create new
from datetime import datetime
credential = GmailCredential(
user_id=current_user.id,
gmail_email=credential_in.gmail_email,
encrypted_access_token=encrypted_access,
encrypted_refresh_token=encrypted_refresh,
is_valid=True,
last_verified_at=datetime.utcnow(),
)
db.add(credential)
await db.commit()
await db.refresh(credential)
return credential
@router.get("/gmail-credential", response_model=GmailCredentialResponse)
async def get_gmail_credential(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Get the current user's Gmail API credential status"""
result = await db.execute(
select(GmailCredential).where(GmailCredential.user_id == current_user.id)
)
credential = result.scalar_one_or_none()
if not credential:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No Gmail credentials configured. Set up Gmail API access first.",
)
return credential
@router.delete("/gmail-credential", status_code=status.HTTP_204_NO_CONTENT)
async def delete_gmail_credential(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Delete the current user's Gmail API credentials"""
result = await db.execute(
select(GmailCredential).where(GmailCredential.user_id == current_user.id)
)
credential = result.scalar_one_or_none()
if not credential:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No Gmail credentials found",
)
await db.delete(credential)
await db.commit()