ADR-0009: Autenticazione IMAP — OAuth2 first per Gmail e Microsoft 365
Status
Accepted — implementazione obbligatoria dal day 1 della pipeline ingestion email.
Context
Akira ingestisce email da caselle dedicate per più moduli:
- NOC Trouble Tickets (cap. 16.6): mailbox
noc@<dominio>con polling IMAP, match company by sender, creazione TT automatica. - Quality Verifier Open Ticket (cap. 49.2.4): risposta a email outbound.
- Pattern Analyzer / fraud reports (cap. 58.9): mailbox dedicata per ricezione report carrier.
Tutti i provider enterprise hanno dismesso basic auth IMAP o sono in fase di step-down:
- Google Gmail / Google Workspace: basic auth IMAP disabilitato da maggio 2022. Obbligatorio OAuth2 / XOAUTH2.
- Microsoft 365 / Exchange Online: basic auth disabilitato per nuovi tenant da ottobre 2022, step-down completato su tenant esistenti nel 2023. Obbligatorio OAuth2 XOAUTH2 con tenant_id.
- Yahoo Mail: solo app password (legacy ma supportato).
- Provider IT (Aruba, RegisterMail, Register.it): ancora password legacy IMAP.
Implementare solo basic auth significherebbe escludere la maggior parte dei customer enterprise dal day 1.
Decision
Implementare OAuth2 IMAP day-1 come prima opzione, con fallback password legacy per provider che non lo supportano ancora.
Provider OAuth2 supportati
| Provider | Mechanism | Note |
|---|---|---|
| Gmail / Google Workspace | XOAUTH2 | Google Cloud Console OAuth app, scope https://mail.google.com/ |
| Microsoft 365 / Exchange Online | XOAUTH2 | Azure AD app registration, scope https://outlook.office.com/IMAP.AccessAsUser.All, richiede tenant_id |
Provider password legacy (transitorio)
- Aruba, RegisterMail, Register.it, generic IMAP/SSL.
- Marcati
deprecation roadmap v1.1— quando vorranno fare onboarding di customer su questi provider, segnalare in UI.
Schema DB — tabella mailboxes
CREATE TABLE mailboxes (
id UUID PRIMARY KEY,
company_id UUID NOT NULL REFERENCES companies(id),
purpose VARCHAR NOT NULL, -- 'noc_tt', 'quality_verifier', 'fraud_reports'
email_address VARCHAR NOT NULL,
imap_host VARCHAR NOT NULL,
imap_port INT NOT NULL DEFAULT 993,
imap_use_ssl BOOLEAN NOT NULL DEFAULT TRUE,
auth_type VARCHAR NOT NULL, -- 'basic_password' | 'oauth2'
-- OAuth2 fields (NULL se basic_password)
oauth_provider VARCHAR, -- 'gmail' | 'microsoft365'
oauth_tenant_id VARCHAR, -- solo microsoft365
oauth_client_id VARCHAR,
oauth_refresh_token_encrypted BYTEA, -- AES-256-GCM
oauth_access_token_cache_encrypted BYTEA,
oauth_token_expires_at TIMESTAMPTZ,
-- Basic password fields (NULL se oauth2)
password_encrypted BYTEA, -- AES-256-GCM, deprecation v1.1
polling_interval_seconds INT NOT NULL DEFAULT 60,
last_poll_at TIMESTAMPTZ,
last_poll_status VARCHAR,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT auth_type_consistency CHECK (
(
auth_type = 'basic_password'
AND password_encrypted IS NOT NULL
AND oauth_refresh_token_encrypted IS NULL
)
OR
(
auth_type = 'oauth2'
AND password_encrypted IS NULL
AND oauth_provider IS NOT NULL
AND (oauth_provider <> 'microsoft365' OR oauth_tenant_id IS NOT NULL)
AND (oauth_refresh_token_encrypted IS NOT NULL OR is_active = FALSE)
)
)
);
Cifratura at-rest
- Algoritmo: AES-256-GCM.
- Chiave:
ENCRYPTION_KEYdall'env (32 byte base64), gestita via Ansible vault. - Nonce: random 12 byte, prepended al ciphertext.
- Tag GCM: appended al ciphertext.
- Layout BYTEA:
[12B nonce][ciphertext][16B tag].
Token rotation
- Refresh token Gmail: long-lived, rotation automatica ogni 90gg (cron arq job
oauth_token_rotator). - Refresh token Microsoft 365: long-lived (default 90gg, configurabile lato Azure AD). Stessa rotation 90gg.
- Access token cache: refresh on-demand quando expired (Pydantic check
oauth_token_expires_at < now() - 60s).
Admin UI flow OAuth2
- Admin clicca "Add mailbox" → seleziona provider (Gmail / M365 / Other) e inserisce i campi IMAP.
- Se OAuth2, il backend crea subito la riga
mailboxesconauth_type='oauth2',oauth_provider, eventualeoauth_tenant_id, password assente, token assente,is_active=falseelast_poll_status='pending_authorization'. - Admin clicca "Autorizza" sulla riga pending; la UI chiama
POST /api/v1/admin/mailboxes/{id}/oauth/start?provider=.... - Backend genera
statefirmato conmailbox_ide provider, redirecta al provider consent URL. Per Microsoft 365 usaoauth_tenant_iddella mailbox se presente. - Callback
/api/v1/admin/mailboxes/oauth/callbackricevecode, verificastate, scambia per refresh+access token e salva i token cifrati sulla riga esistente. - La mailbox autorizzata diventa attivabile dall'admin. Finché
oauth_refresh_token_encryptedè NULL,PATCH is_active=trueè bloccato con errore esplicito e il poller non tenta login IMAP. - Test connessione IMAP immediato, mostra esito.
Consequences
Positive
- Compatibilità enterprise day-1: Gmail Workspace e M365 (90%+ dei tenant business europei).
- Security: nessuna password plaintext, token revocabili lato provider.
- Audit: ogni accesso traceable via OAuth app log lato provider.
Negative
- Setup iniziale complesso: registrare app Google Cloud Console + Azure AD app. Documentation onboarding necessaria.
- Quote OAuth provider: Google ha quota per refresh token; M365 ha rate limit. Da monitorare con > 50 mailbox.
- Cron rotation: nuovo job arq da implementare e monitorare.
Neutral
- Doppio code path (oauth2 vs basic_password) → factory pattern
IMAPAuthStrategy. - Migration futura: quando vorremo deprecare basic_password, query
SELECT count(*) FROM mailboxes WHERE auth_type='basic_password'ci dice quanti customer impattati.
Implementation hints
Librerie Python
- IMAP client:
aioimaplib(async). - OAuth2 flow:
httpxper token exchange. - AES-256-GCM:
cryptographypackage,AESGCMclass.
Helper class
class MailboxCredentialResolver:
async def get_imap_auth(self, mailbox_id: UUID) -> tuple[str, str]:
"""Returns (username, auth_string) for aioimaplib login."""
mb = await self.db.fetch_mailbox(mailbox_id)
if mb.auth_type == 'oauth2':
access = await self._refresh_if_needed(mb)
xoauth2 = self._build_xoauth2(mb.email_address, access)
return mb.email_address, xoauth2
else:
pwd = self._decrypt(mb.password_encrypted)
return mb.email_address, pwd
Google Cloud Console setup
- Progetto:
akira-prod. - OAuth consent screen: External (se customer hanno Workspace) o Internal.
- Scope:
https://mail.google.com/. - Redirect URI:
https://akira.example.com/admin/mailboxes/oauth/callback.
Azure AD setup
- App registration:
Akira IMAP Ingestion. - API permissions:
Office 365 Exchange Online→IMAP.AccessAsUser.All(delegated) eoffline_access. - Admin consent richiesto lato tenant customer.
Env vars
ENCRYPTION_KEY=... # base64 32 byte
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=...
MICROSOFT_OAUTH_CLIENT_ID=...
MICROSOFT_OAUTH_CLIENT_SECRET=...
Monitoring
- Metric
mailbox_oauth_refresh_failed_total{provider=}→ alert se > 0 in 5min. - Metric
mailbox_poll_duration_seconds{mailbox_id=}→ SLO < 30s.
Alternatives considered
- Solo basic password: scartato — Gmail/M365 non lo supportano più, escluderemmo i customer enterprise.
- App-specific password (Yahoo style): scartato per Gmail/M365 — non supportato sul piano Workspace/Business.
- MS Graph API invece di IMAP: rivalutabile post-MVP — più ricco di metadata, ma cambia il code path drasticamente. Per ora IMAP unificato OK.
- Tenant per-customer app registration: scartato — operational nightmare, una sola app multi-tenant è la prassi.
- Storage refresh token plaintext: scartato per ovvi motivi di security (compromise mailbox = compromise account).