- Shipped
- September 23, 2026 at 2:35 AM UTC
- Author
- Kamo
- Commit
- a0d3c42
POST /api/email/send took request.getFromAddress() straight into the header From and the SMTP envelope sender **************** -> EmailSmtpService.send, and the same override in SmtpOnlyProvider.send for orgs relaying through their own SMTP), with no check that the caller had any claim to it. The cluster relay DKIM-signs by header domain, so a hand-crafted @kamocrm.com From arrived DMARC-aligned as anyone — another member, an executive, or another org's mailbox entirely. EmailController#sendEmail now asks SendableMailboxService — the same enumeration the compose picker already pages through to offer From choices — whether the resolved member may send as the requested address: their own mailbox, an alias they own, a shared mailbox they were granted, or a **************** privilege grant on someone else's. An address outside that set is refused with 403 before the provider ever sees the message. An unset From is unaffected — it still means the connection's own address, which provider resolution already gated. Checked every other path that can carry a From: reply/forward and saveDraft never accept an override at all (always the connection's own address); **************** ignores the request body's fromAddress and always sends as the granted shared mailbox; campaign and drip sends compute their From from the campaign's own configured/verified sending identity, not from per-request input. **************** and **************** fail without this change — a spoofed From currently sends (200) instead of being refused, and canSendAs would authorize an address that matches none of the member's groups.
