agentic.email/backend/app/main.py
2026-10-05 04:53:45 +05:30

261 lines
7.9 KiB
Python

from contextlib import asynccontextmanager
from fastapi import FastAPI, Response, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from app.config import get_settings
from app.database import SessionFactory, engine, mongo_client, mongo_db
from app.models import Base
from app.routers import (
aliases,
auth,
dns_setup,
domains,
emails,
groups,
org,
people,
sections,
sticky_notes,
)
from app.services.domain_provisioning import ensure_platform_domain
from app.services.inbound_smtp import InboundSMTPServer
settings = get_settings()
inbound_server = InboundSMTPServer()
async def migrate_mail_group_columns(connection) -> None:
await connection.execute(
text(
"ALTER TABLE mail_groups ADD COLUMN IF NOT EXISTS owner_id UUID "
"REFERENCES users(id)"
)
)
await connection.execute(
text(
"ALTER TABLE mail_groups ADD COLUMN IF NOT EXISTS send_permission "
"VARCHAR(30) DEFAULT 'org_members'"
)
)
await connection.execute(
text("ALTER TABLE mail_groups ADD COLUMN IF NOT EXISTS description TEXT")
)
async def migrate_user_org_columns(connection) -> None:
await connection.execute(
text("ALTER TABLE users ADD COLUMN IF NOT EXISTS org_role VARCHAR(20)")
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS member_status VARCHAR(20)"
)
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS created_by_id UUID "
"REFERENCES users(id)"
)
)
await connection.execute(
text("UPDATE users SET org_role = 'user' WHERE org_role IS NULL")
)
await connection.execute(
text("UPDATE users SET member_status = 'active' WHERE member_status IS NULL")
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS invitation_email VARCHAR(320)"
)
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN "
"DEFAULT FALSE"
)
)
await connection.execute(
text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS section_id UUID "
"REFERENCES org_sections(id)"
)
)
async def migrate_legacy_user_tables(connection) -> None:
"""Promote the former auth table to users without discarding old profiles."""
tables = (
await connection.execute(
text(
"SELECT to_regclass('public.accounts'), "
"to_regclass('public.users'), "
"to_regclass('public.legacy_users')"
)
)
).one()
accounts_table, users_table, legacy_users_table = tables
if accounts_table is None:
return
if users_table is not None:
if legacy_users_table is not None:
raise RuntimeError(
"Cannot migrate accounts: both users and legacy_users already exist."
)
await connection.execute(text("ALTER TABLE users RENAME TO legacy_users"))
await connection.execute(text("ALTER TABLE accounts RENAME TO users"))
@asynccontextmanager
async def lifespan(_: FastAPI):
async with engine.begin() as connection:
await migrate_legacy_user_tables(connection)
await connection.run_sync(Base.metadata.create_all)
await migrate_user_org_columns(connection)
await migrate_mail_group_columns(connection)
async with SessionFactory() as session:
await ensure_platform_domain(session)
await session.commit()
await mongo_db.emails.create_index([("recipients", 1), ("created_at", -1)])
await mongo_db.emails.create_index([("cc", 1), ("created_at", -1)])
await mongo_db.emails.create_index([("bcc", 1), ("created_at", -1)])
await mongo_db.emails.create_index([("sender", 1), ("created_at", -1)])
await mongo_db.emails.create_index([("account_id", 1), ("created_at", -1)])
await mongo_db.messages.create_index(
[("delivery_key", 1)],
unique=True,
sparse=True,
)
await mongo_db.message_tombstones.create_index(
[("delivery_key", 1)],
unique=True,
)
await mongo_db.messages.create_index(
[("subject", "text"), ("snippet", "text"), ("sender.name", "text")]
)
await mongo_db.mailbox_entries.create_index(
[
("account_id", 1),
("mailbox", 1),
("trashed_at", 1),
("deletion_state", 1),
("created_at", -1),
("_id", -1),
]
)
await mongo_db.mailbox_entries.create_index(
[("account_id", 1), ("is_read", 1), ("created_at", -1), ("_id", -1)]
)
await mongo_db.mailbox_entries.create_index(
[("account_id", 1), ("message_id", 1), ("mailbox", 1)],
unique=True,
)
await mongo_db.mailbox_entries.create_index(
[("account_id", 1), ("is_starred", 1), ("created_at", -1), ("_id", -1)]
)
await mongo_db.sticky_notes.create_index(
[("account_id", 1), ("updated_at", -1)]
)
await mongo_db.mailbox_entries.create_index(
[
("account_id", 1),
("sender.name", "text"),
("sender.address", "text"),
("subject", "text"),
("snippet", "text"),
],
name="mailbox_search",
)
if settings.inbound_smtp_enabled:
await inbound_server.start()
try:
yield
finally:
await inbound_server.stop()
await engine.dispose()
await mongo_client.close()
app = FastAPI(
title=settings.app_name,
version="0.1.0",
description="The API for Agmail — email for people and their AI agents.",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(emails.router, prefix=settings.api_prefix)
app.include_router(sticky_notes.router, prefix=settings.api_prefix)
app.include_router(auth.router, prefix=settings.api_prefix)
app.include_router(domains.router, prefix=settings.api_prefix)
app.include_router(people.router, prefix=settings.api_prefix)
app.include_router(sections.router, prefix=settings.api_prefix)
app.include_router(groups.router, prefix=settings.api_prefix)
app.include_router(aliases.router, prefix=settings.api_prefix)
app.include_router(org.router, prefix=settings.api_prefix)
app.include_router(dns_setup.router, prefix=settings.api_prefix)
@app.get("/")
async def root():
return {
"name": settings.app_name,
"docs": "/docs",
"version": "0.1.0",
}
@app.get("/health")
async def health(response: Response):
checks = {
"postgres": False,
"mongodb": False,
"smtp": False,
"inbound_smtp": (
not settings.inbound_smtp_enabled
or (
inbound_server.server is not None
and inbound_server.server.is_serving()
)
),
}
try:
async with SessionFactory() as session:
await session.execute(text("SELECT 1"))
checks["postgres"] = True
except Exception:
pass
try:
await mongo_db.command("ping")
checks["mongodb"] = True
except Exception:
pass
try:
import socket
with socket.create_connection(
(settings.smtp_host, settings.smtp_port),
timeout=3,
):
checks["smtp"] = True
except Exception:
pass
healthy = checks["postgres"] and checks["mongodb"]
if settings.inbound_smtp_enabled:
healthy = healthy and checks["inbound_smtp"]
if settings.smtp_health_required:
healthy = healthy and checks["smtp"]
if not healthy:
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": "healthy" if healthy else "degraded", "checks": checks}