feat(microsoft-shared): Phase 1b — add imap_smtp + imap_leadmail transports
Microsoft 365 Graph API requires the shared mailbox itself to be licensed
(€5/mo each), which doesn't scale across clients. Phase 1's graph_full
transport stays for licensed mailboxes; this commit adds two free-of-extra-
licensing transports alongside it. Admin picks per channel.
Three transports now:
- graph_full -> Graph send + (Phase 2) Graph webhook receive
- imap_smtp -> M365 xoauth2 SMTP send + xoauth2 IMAP receive
- imap_leadmail -> LeadMail API send + xoauth2 IMAP receive
Backend:
- MicrosoftSharedConcern: SCOPES_BY_TRANSPORT map (Graph and Outlook scopes
are different audiences — separate scope set per transport).
- AuthorizationsController accepts and validates a transport param,
uses transport-specific scopes, encodes transport into signed state.
- CallbacksController reads transport from state, dispatches to the right
access verifier (Graph for graph_full, new ImapAccessVerifier for imap_*),
stores transport in provider_config, sets imap_login=upn for IMAP transports.
- New Microsoft::Shared::ImapAccessVerifier opens a Net::IMAP connection,
authenticates XOAUTH2 with the SHARED mailbox UPN as username and the
delegate's access token, runs CAPABILITY, closes. Confirms the SASL pattern
works for this delegate before creating the channel.
Send routing (overlay on ConversationReplyMailerHelper):
- graph_full -> :microsoft_shared delivery (existing Graph send service)
- imap_smtp -> :smtp with xoauth2, delegate UPN as SASL user, channel.email
as From: header (Microsoft routes via Send As permission)
- imap_leadmail -> :leadmail delivery (existing LeadmailDelivery + the small
extension below to forward threading headers)
Receive routing:
- New Custom::Leadchat::Inboxes::FetchImapEmailsJobExtension overlay routes
microsoft_shared channels with imap_* transport to the existing
Imap::MicrosoftFetchEmailService. That service already does xoauth2 IMAP
with channel.imap_login as the SASL user; we set imap_login=upn at channel
creation so the existing fetch logic reads the shared mailbox's INBOX with
no service-level changes.
LeadmailDelivery: build_payload now forwards in_reply_to, references, and
message_id so conversation reply threading works on the imap_leadmail path.
LeadMail server should preserve these as RFC822 headers on the outbound MIME.
Frontend:
- MicrosoftShared.vue gets a transport radio with 3 options + per-option help.
- microsoftSharedClient already passes payload through (no change).
- en inboxMgmt.json: TRANSPORT.{IMAP_SMTP,IMAP_LEADMAIL,GRAPH}.{TITLE,HELP}.
Token management: existing Microsoft::RefreshOauthTokenService works for
all three transports — refresh tokens come back with the same scopes the
original grant had.
Phase 2 (Graph webhook receive for graph_full) remains pending; only needed
if you ship licensed-mailbox channels.
This commit is contained in:
16
LEADCHAT.md
16
LEADCHAT.md
@@ -17,7 +17,8 @@ LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Cus
|
||||
| Hook in OSS file | Overlay file | Adds |
|
||||
|---|---|---|
|
||||
| `app/models/channel/email.rb` (bottom) | `custom/app/models/custom/leadchat/channel/email_extension.rb` | `Channel::Email#microsoft_shared?` predicate |
|
||||
| `app/mailers/conversation_reply_mailer_helper.rb` (bottom) | `custom/app/mailers/custom/leadchat/conversation_reply_mailer_helper_extension.rb` | Routes `microsoft_shared` channels to the `:microsoft_shared` ActionMailer delivery method (Graph API), bypassing xoauth2 SMTP |
|
||||
| `app/mailers/conversation_reply_mailer_helper.rb` (bottom) | `custom/app/mailers/custom/leadchat/conversation_reply_mailer_helper_extension.rb` | Routes `microsoft_shared` channels by `provider_config['transport']`: `graph_full` → `:microsoft_shared` delivery (Graph), `imap_smtp` → `:smtp` with xoauth2 (delegate UPN as SASL user, channel.email as From: for Send As), `imap_leadmail` → `:leadmail` |
|
||||
| `app/jobs/inboxes/fetch_imap_emails_job.rb` (bottom) | `custom/app/jobs/custom/leadchat/inboxes/fetch_imap_emails_job_extension.rb` | Routes `microsoft_shared` channels with `transport ∈ {imap_smtp, imap_leadmail}` to `Imap::MicrosoftFetchEmailService` (which already handles xoauth2 token refresh) |
|
||||
|
||||
### B. Direct edits to OSS files
|
||||
|
||||
@@ -26,6 +27,8 @@ LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Cus
|
||||
| `config/application.rb` | Mirror enterprise/ autoload setup for `custom/` so `Custom::*` modules are autoloaded and `custom/config/initializers/**/*.rb` are required | `# === LeadChat: custom/ overlay autoload ===` |
|
||||
| `app/models/channel/email.rb` (bottom) | One-line `include_mod_with` hook for the email_extension overlay | `# === LeadChat: microsoft_shared ===` |
|
||||
| `app/mailers/conversation_reply_mailer_helper.rb` (bottom) | One-line `prepend_mod_with` hook for the helper extension overlay | `# === LeadChat: microsoft_shared ===` |
|
||||
| `app/jobs/inboxes/fetch_imap_emails_job.rb` (bottom) | One-line `prepend_mod_with` hook for the fetch dispatcher overlay | `# === LeadChat: microsoft_shared ===` |
|
||||
| `app/mailers/leadmail_delivery.rb` (`build_payload`) | Forward `In-Reply-To`, `References`, and `Message-ID` headers in the LeadMail API payload so conversation replies thread correctly when sent via the `imap_leadmail` transport | `# === LeadChat: microsoft_shared ===` |
|
||||
| `config/routes.rb` (3 separate blocks) | (1) `app_new_microsoft_shared_inbox` dashboard route; (2) account-namespaced `microsoft_shared/authorization` API endpoint; (3) `microsoft_shared/callback` OAuth callback | `# === LeadChat: microsoft_shared ===` |
|
||||
| `app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue` (3 separate blocks) | (1) import `MicrosoftShared.vue`; (2) `emailProviderList` entry for `microsoft_shared`; (3) router slot `<MicrosoftShared v-else-if="provider === 'microsoft_shared'"/>` | `<!-- === LeadChat: microsoft_shared === -->` and `// === LeadChat: microsoft_shared ===` |
|
||||
| `app/javascript/dashboard/i18n/locale/en/inboxMgmt.json` | New keys: `INBOX_MGMT.ADD.MICROSOFT_SHARED.*` and `INBOX_MGMT.EMAIL_PROVIDERS.MICROSOFT_SHARED.*` | (no comment markers — JSON; flagged here in LEADCHAT.md instead) |
|
||||
@@ -40,10 +43,11 @@ LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Cus
|
||||
| `app/controllers/concerns/microsoft_shared_concern.rb` | OAuth client + Graph scopes + signed-state encoding/decoding for the new provider |
|
||||
| `app/controllers/api/v1/accounts/microsoft_shared/authorizations_controller.rb` | Accepts shared mailbox UPN, returns Microsoft authorize URL with UPN embedded in signed state |
|
||||
| `app/controllers/microsoft_shared/callbacks_controller.rb` | OAuth callback: decodes state, exchanges code, verifies UPN access via Graph, creates `Channel::Email` + `Inbox` |
|
||||
| `app/services/microsoft/shared/send_mail_service.rb` | Builds RFC 822 MIME and POSTs base64-encoded to `/users/{upn}/sendMail` via Graph |
|
||||
| `app/services/microsoft/shared/send_mail_service.rb` | Builds RFC 822 MIME and POSTs base64-encoded to `/users/{upn}/sendMail` via Graph (used by `graph_full` transport) |
|
||||
| `app/services/microsoft/shared/imap_access_verifier.rb` | Verifies a delegate's access token can scope IMAP to a shared mailbox (used by callback for `imap_*` transports) |
|
||||
| `app/mailers/microsoft_shared_delivery.rb` | Custom ActionMailer delivery method that delegates to the send service |
|
||||
| `app/javascript/dashboard/api/channel/microsoftSharedClient.js` | Frontend API client |
|
||||
| `app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/MicrosoftShared.vue` | Single-step UPN entry form before the OAuth redirect |
|
||||
| `app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/MicrosoftShared.vue` | Wizard form: UPN entry + transport radio (3 presets: imap_smtp, imap_leadmail, graph_full) |
|
||||
|
||||
### D. Brand script targets
|
||||
|
||||
@@ -53,7 +57,11 @@ Strings/files that the brand script rewrites away from "Chatwoot". Extend this l
|
||||
|
||||
## Active feature work
|
||||
|
||||
- **Microsoft Shared Inbox provider** (in progress) — new `microsoft_shared` Email Channel type backed entirely by Microsoft Graph API, supports multiple shared mailboxes per OAuth via separate channel-add flows, bypasses Security Defaults / SMTP AUTH retirement. Plan: `~/.claude/plans/starry-mapping-snowflake.md`.
|
||||
- **Microsoft Shared Inbox provider** (Phase 1b in progress) — new `microsoft_shared` Email Channel type with three transport presets selected per channel:
|
||||
- `graph_full` — Graph send + Graph webhook receive (requires shared mailbox license; Graph send shipped, webhook receive is Phase 2)
|
||||
- `imap_smtp` — M365 xoauth2 SMTP send + xoauth2 IMAP receive (no license needed; admin enables SMTP AUTH + IMAP per delegate user)
|
||||
- `imap_leadmail` — LeadMail API send + xoauth2 IMAP receive (no license needed, no M365 SMTP AUTH needed)
|
||||
Plan: `~/.claude/plans/starry-mapping-snowflake.md`.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user