Skip to content

service: inbound mail + calendar sync (open-core scope) — plugin-email is outbound-only, so email-to-record is impossible in any app #8998

Description

@os-zhuang

Current state (verified)

plugin-email is a complete outbound stack and nothing else:

  • src/transports/smtp.ts, postmark.ts, resend.ts. All send-side.
  • Template engine (mustache-style with formula formatters), outbox sweep, queue delivery, attachment storage.

There is no inbound path at all: no IMAP / EWS / Graph client, no mailbox sync loop, no message ingest, no association of a received message to a record. service-calendar does not exist either — apps can render a calendar view over their own event object, but nothing synchronises with an external calendar.

Consequence: "the customer's emails show up on the customer record" cannot be built on this platform today, in any app, by any means short of a bespoke integration per deployment.

Why this belongs in the open core

Found while scoping a bank CRM RFI (VTB Shanghai SME), where it was the single largest technical risk and the critical-path item of phase 1 — quoted at ~60 person-days of custom work. But the reason to put it in core is not that one deal:

  • HotCRM is the marketplace flagship and it is demonstrably incomplete without it. Email-to-record is the first thing any CRM evaluator tries. "Requires a custom integration" at that moment ends the evaluation.
  • Every comparable open CRM (SuiteCRM, EspoCRM, Odoo) ships inbound mail in its free tier. Gating it would put us behind the free alternatives at exactly the point where a prospect is comparing.
  • It is protocol plumbing, not governance. It fails the "the buyer is the IT/compliance function" test that justifies enterprise tier — the person who feels this feature is the end user, every day.

Scope split (agreed): this issue covers the standards-based layer only. Tenant-scale administration — Exchange/Graph tenant-wide delegation, Google domain-wide delegation, bulk mailbox provisioning, sync health monitoring — is enterprise-tier and will be filed separately. Rule of thumb we settled on: one person connecting their own IMAP mailbox is core; taking over 500 Exchange mailboxes at once is enterprise.

Proposed architecture

Three layers, so that the enterprise tier extends rather than forks this work:

1. Connector layer — protocol + auth only, following the existing packages/connectors/ convention (connector-rest, connector-openapi, connector-mcp, connector-slack):

  • connector-imap (+ SMTP for the send side already covered by plugin-email)
  • connector-caldav
  • Enterprise adds connector-exchange / connector-graph / connector-google on the same interface.

2. Service layer — the sync engines, sharing the connector's account + credentials:

  • service-mailbox — folder cursors / UID state, incremental fetch, dedup, threading, attachment extraction, bounce & auto-reply suppression.
  • service-calendar — event sync, recurrence (RRULE) expansion, timezone correctness, attendee/RSVP state, optional write-back.

They are separate services because the sync semantics genuinely differ (mail is read-mostly; calendar is commonly bidirectional) and a customer may want one without the other — but they share one account and one auth layer, hence the shared connector.

3. Association seam — the part that must NOT live in the platform.

The platform has no business knowing what a "contact" or an "account" is. It should deliver normalised messages/events into platform-level objects and expose an extension point of the shape "given these participant addresses, which record(s) does this belong to?", which the app answers. HotCRM would resolve to crm_contact / crm_account and mount onto its existing crm_event / crm_event_attendee.

This follows the precedent already set by sys_email (a platform-level outbound queue object that apps write into): the inbound mirror would be platform-owned (sys_mail_message, sys_calendar_event), with the app's own business object associated to it rather than replaced by it.

Design decisions to settle

  1. Mirror vs. adopt — does the app relate its own event object to the platform's synced event, or does the platform's object become the app's? Mirror avoids forcing every app into one event shape but costs a join and a sync direction.
  2. Association timing — resolve at ingest (stamp the link) or at read (resolve dynamically)? Ingest is faster to read and survives contact-record changes badly; read-time is the reverse.
  3. Unmatched messages — drop, or keep in an unassociated pool for later matching when the contact is created? (CRM users expect the latter.)
  4. Shared vs. per-user mailboxes — the lightweight pattern (one dedicated mailbox that users forward or BCC into, parsed and associated) is materially cheaper than per-user sync and covers a lot of real demand. Worth shipping first; it is also what we scoped as the phase-1 fallback on the RFI above.
  5. Privacy posture — syncing a user's whole mailbox into a shared CRM is a real concern in some jurisdictions. Need a declarable policy (folder allowlist, participant-must-be-a-known-contact, explicit per-message share).

Implementation will likely split into sub-issues per connector and per service; filing as one issue so the layering and the seam get designed once rather than twice.

Verified against

0f539bdpackages/plugins/plugin-email/src/ (transports, template-engine, outbox-sweep), packages/connectors/, packages/services/.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions