Files
leadchat/LEADCHAT.md
netlas 0839fbec79
Some checks failed
Lock Threads / action (push) Has been cancelled
docs(leadchat): document LeadMail delivery_method fix and LeadmailDelivery patch in customization index
Both LeadMail-related touchpoints now logged in section B (Direct edits
to OSS files). Notes the LeadChat-owned vs upstream distinction so
future merges know which need defense and which are ours to evolve.
2026-04-27 19:57:16 +03:00

11 KiB

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.rbChatwootApp.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).