# LeadChat customizations

This file is the index of every LeadChat-specific customization on top of upstream Chatwoot. Consult it during upstream merges to know what's customized and where conflicts may surface.

LeadChat is a white-label Chatwoot fork maintained on the `leadchat` branch. Customizations are layered using Chatwoot's built-in `custom/` extension mechanism (see [`lib/chatwoot_app.rb`](lib/chatwoot_app.rb) — `ChatwootApp.extensions` includes `'custom'` automatically when a `custom/` directory exists at repo root).

## Layering strategy

1. **Overlay modules under `custom/`** — preferred for Ruby modifications. New code lives in `custom/app/...` under the `Custom::Leadchat::*` namespace; OSS files only get a one-line `include_mod_with` / `prepend_mod_with` hook that Chatwoot's extension loader resolves automatically.
2. **Direct edits to OSS** — only where no overlay mechanism exists (routes, schedule, Vue components). Each block is wrapped in `# === LeadChat: <feature> (start/end) ===` markers so future merge conflicts are mechanical.
3. **Brand script** — separate process that overwrites brand strings in upstream files even after they're updated. Listed in "Brand script targets" below so we can extend its rules as new strings surface.

## Customization categories

### A. Overlay modules (Custom::Leadchat::*)

| 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 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

| File | Change | Marker |
|---|---|---|
| `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. (LeadmailDelivery is LeadChat-owned code — direct edit is acceptable; overlay would be over-engineering.) | `# === LeadChat: microsoft_shared ===` |
| `config/initializers/mailer.rb` (LeadMail and SMTP/sendmail branches) | Apply `delivery_method` and matching `*_settings` directly to `ActionMailer::Base`, not just `config.action_mailer.*`. Required because `devise.rb` runs alphabetically before `mailer.rb` and triggers ActionMailer's `on_load` hook with the default `:smtp` config — subsequent writes to `config.action_mailer.delivery_method` are silently ignored. Without this fix, all transactional email (Devise confirmation, password reset, invitations) silently fails with `Errno::ECONNREFUSED` to `localhost:25`. | `# === LeadChat: leadmail-delivery-fix ===` |
| `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) |

### C. New top-level files (no upstream conflict potential)

| Path | Purpose |
|---|---|
| `LEADCHAT.md` | This file |
| `custom/` | Root of LeadChat overlay tree, picked up by `ChatwootApp.custom?` and the Phase-0 autoload setup |
| `custom/config/initializers/microsoft_shared_delivery.rb` | Registers `:microsoft_shared` ActionMailer delivery method |
| `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 (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` | Wizard form: UPN entry + transport radio (3 presets: imap_smtp, imap_leadmail, graph_full) |

### D. Brand script targets

Strings/files that the brand script rewrites away from "Chatwoot". Extend this list whenever a new "Chatwoot" mention surfaces in product UI.

- `app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue` lines 33, 43 — hardcoded `businessName: 'Chatwoot'` in sender-name preview tiles. (Discovered 2026-04-27 while building the Microsoft Shared Inbox feature; not yet handled by the brand script.)

## Active feature work

- **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

- Conventional Commits (`type(scope): subject`)
- No Claude attribution in commits or PR bodies
- Update this file as each customization lands, not as a final pass

## Phase 1b smoke-test checklist (run after staging deploy)

Pre-req on M365 admin:
- Delegate user (e.g. Niklas) has `Send As` + `Full Access` on the test shared mailbox.
- For `imap_smtp`: enable both **Authenticated SMTP** and **IMAP** in Active users → user → Mail → Manage email apps. Confirm no Conditional Access policy blocks them.
- For `imap_leadmail`: enable **IMAP** only.
- For `graph_full`: assign a Microsoft 365 license to the shared mailbox itself. (Skip this transport if you don't want to license shared mailboxes.)

Deployment:
1. `cd /home/ploi/chat.leadm.app && git pull origin leadchat`
2. Free RAM (stop Sidekiq + Site + gitea via Ploi/systemctl), then:
   `NODE_OPTIONS="--max-old-space-size=3072" RAILS_ENV=production bundle exec rake assets:precompile`
3. Restart Sidekiq, Site, gitea.

Regression check (do FIRST, before testing the new feature):
- Trigger any transactional email that goes through LeadMail (e.g. invite a new user, or trigger a password-reset email). Confirm it still arrives normally. The LeadmailDelivery patch added `in_reply_to`, `references`, and `message_id` to the API payload — if your LeadMail server rejects unknown fields, this could regress all transactional email. If transactional emails fail, revert just the LeadmailDelivery part of commit `3c93714f2` until LeadMail server tolerates extra fields.

Smoke test `imap_smtp` (highest priority):
1. Add Inbox → Email → Microsoft Shared Inbox → enter shared UPN → pick "SMTP + IMAP" transport → Connect.
2. OAuth consent screen should list `IMAP.AccessAsUser.All` and `SMTP.Send` (Outlook scopes).
3. After redirect: assert `Channel::Email` row has `provider='microsoft_shared'`, `imap_login=<shared upn>`, `provider_config['transport']='imap_smtp'`.
4. External email → shared mailbox → conversation appears in LeadChat within ~1 minute.
5. Reply from LeadChat → recipient receives email From: shared mailbox address; threading headers preserved.
6. Watch `tail -f /home/ploi/chat.leadm.app/log/production.log | grep -iE 'microsoft_shared|imap'` for errors.

Smoke test `imap_leadmail`:
1. Same as above but pick "LeadMail send" transport.
2. After OAuth, scopes should be `IMAP.AccessAsUser.All` only.
3. Reply from LeadChat → verify in LeadMail's outbox/log that the email went via LeadMail (not M365 SMTP).
4. Verify From: header is the shared mailbox; verify DKIM passes for `leadmagnet.fi` (Gmail's "Show original" view shows DKIM=PASS).
5. External replies to your LeadMail-sent message → assert it threads back into the same Chatwoot conversation. If threading fails, the LeadMail server isn't honoring the new `in_reply_to`/`references` payload fields — needs a server-side fix.

`graph_full` (only if you license a mailbox to test):
1. Same as above, "Microsoft Graph" transport.
2. Send works; receive doesn't until Phase 2 webhook subscription is built.

Phase 2 + Phase 3 deferred — only needed if `graph_full` becomes a real use case (Phase 2) or polish UX surfaces issues with re-auth / cleanup (Phase 3).
