VEXA_MAIL_TRANSPORT picks how Vexa reads it; any other value is a startup error.
gmail and imap
Exchange Online with IMAP enabled is four values:
outlook.office365.com:993,
smtp.office365.com:587, VEXA_MAIL_SMTP_STARTTLS=1.
graph
Admin runbook — Microsoft Graph
1 · Register an application. Entra ID → App registrations → New registration. Single tenant, no redirect URI. Record the Application (client) ID and Directory (tenant) ID; create a client secret and record its value, not its id. 2 · Grant application permissions. API permissions → Microsoft Graph → Application permissions (not delegated):Mail.ReadWrite (read the inbox, draft the reply) and
Mail.Send. Nothing else is required to read a mailbox. Replies are drafted then sent because
that is the only way Graph returns the real internetMessageId that keeps a reply in thread.
3 · Grant admin consent for the tenant. Until a Global Administrator (or Privileged
Role Administrator) consents, the app authenticates fine and returns 403 on every call.
4 · Scope the app to one mailbox — required. Mail.ReadWrite as an application
permission is tenant-wide: without this step the app can read every mailbox in your tenant.
Granted for the vexa mailbox, and run the test against a second mailbox too,
expecting Denied — a policy that was created but does not apply looks identical to one that
works. Allow a few minutes for propagation.
Mint the client secret in the admin’s browser and put it into the cluster Secret from the
admin’s terminal. It goes in no ticket, chat or email, Vexa’s included.
az login is not your app. The Azure CLI gives you a delegated token as yourself, and
delegated calls succeed against mailboxes the application permission does not cover. Test with
grant_type=client_credentials and scope=https://graph.microsoft.com/.default. And a fresh
registration returning 403 is usually missing consent, not missing permissions.
If you also want Vexa to create Teams meetings
A separate capability with its own approval path; the mail transport never touches the meetings API. Additional application permissions, with admin consent:OnlineMeetings.ReadWrite.All and Calendars.ReadWrite (to place it on the organizer’s
calendar). Plus a Teams policy granted to the organizing user:
POST /users/{organizer}/onlineMeetings needs the directory object id; a UPN fails with an
invalid-GUID message that reads like a malformed request id. Resolve it once with
GET /users/<upn>?$select=id and store the GUID.
Current limits
- An invite is picked up only if it carries a
https://meet.google.com/…link. A Teams-linked Outlook invite is ignored, silently. - Invites more than 24 hours in the past are dropped.
- First boot anchors at the tail; existing mail is never replayed.
- Delta mode can re-deliver a batch after a crash; de-duplication is by
Message-ID. - The Graph invite-synthesis path is fixture-proven only.
- One manual schema change on an existing deployment:
ALTER TABLE mail_cursor ADD COLUMN token TEXT;. IMAP works without it; Graph refuses to start rather than rewinding your cursor. - The chart does not carry these variables yet.