Connected email accounts
This capability is available for demo purposes only. Its navigation entry is feature-flagged and hidden in the sandbox and production environments.
Brokers can connect their email accounts (Gmail or Outlook) to Korint. The connected email accounts page gives a logged-in broker a single view of the accounts available to them, so they can tell at a glance which mailboxes are linked and whether they are personal or shared with colleagues.
The page lives in the intermediary dashboard under Emails → Gestion des comptes.
What a broker sees
The page requires an authenticated broker within a tenant, and it lists two groups:
- Mes comptes: the broker's personal accounts (
isShared: false). - Comptes partagés: the brokerage firm's shared accounts (
isShared: true), including those connected by the logged-in broker. Shared accounts belong to the firm; each row shows who last connected the account.
Both sections remain visible when empty. The sections use the sharing mode, not the identity of the person who connected the account. There is no lock or sharing toggle.
Within each group, accounts are ordered by connection date, oldest first. Every row shows the provider logo, the account name, its email address, and a status indicator for the connection health.
The page shows a loading state while fetching the list, an empty state in each section with no accounts, and an error state when the list cannot be loaded.
Connect and manage accounts
Each section has its own Ajouter button. The shared section's button is visible only when the broker has the shared-account management permission. Both buttons open the same provider dialog with three choices: Gmail, Outlook, and Boîte partagée Microsoft 365. The personal section starts a personal connection; the shared section starts a shared connection and explains that the mailbox, including its history, is shared with the firm.
A Microsoft 365 shared mailbox has no login of its own, so the third choice first asks for the mailbox address, then sends the broker to Microsoft to sign in with their own account, which must have full access to that mailbox. Korint binds that sign-in to the shared mailbox: the connected account carries the mailbox address, not the broker's, and its mail is synchronized directly. This choice is independent of the section: a Microsoft 365 shared mailbox can be connected as a personal account, and a regular Outlook mailbox as a shared one. Reconnecting such an account repeats the same delegated sign-in.
You can disconnect your personal accounts and reconnect them when their status
is error. Shared-account management requires the firm's
EMAIL_ACCOUNT_SHARED_ADMIN permission, even for the broker who originally
connected the account. A permitted administrator can disconnect or reconnect
a shared account connected by a colleague. A disconnected status indicates
an automatic retry and does not show the reconnect action.
A mailbox can be connected only once per brokerage firm for a given provider and email address. To change its sharing mode, disconnect it, then use Ajouter in the other section. This reuses the deleted account and applies the selected sharing mode to its history.
Feature flag and environment policy
Visibility is controlled by the feat_connected_email_accounts feature flag,
resolved through the in-house feature-flag registry. The flag is a row in the
FeatureFlag table, evaluated per environment, and defaults to disabled when no
row is present.
Because each environment has its own FeatureFlag table, the section is enabled
where it is wanted (local, staging, review environments) and left disabled in
sandbox and production, where no row is created. Operators who wonder why the
Emails navigation is or is not visible should check this flag in the target
environment.
Gateway endpoint
The list is served by GET /email-accounts/@me. It resolves the current broker
from the authenticated session and the tenant, and returns the broker's own
accounts plus every account shared within the broker's brokerage firm. The
response also supplies canManageSharedAccounts, which controls the shared
section's management actions, and connectedBy on each account.
Both add buttons use POST /email-accounts/connect with the selected provider,
the frontend return URL, and isShared. A Microsoft 365 shared mailbox adds
delegatedMailboxAddress; it is accepted with the outlook provider only and
rejected with DELEGATED_MAILBOX_REQUIRES_OUTLOOK otherwise. Reconnection uses
POST /email-accounts/:emailAccountId/reconnect and preserves the sharing mode;
disconnection uses DELETE /email-accounts/:emailAccountId. The server enforces
the permissions independently of the UI. These endpoints are internal and are
not part of the public partner API.
How connected messages are synchronized
You can receive message metadata before its body is available. Korint ingests mailbox events and retrieves bodies asynchronously; there is no public API operation to trigger body retrieval.
- A
messageNewevent ingests the message only when its account is active and has not been deleted. If account creation is still pending, processing fails so the background job can retry. - Ingestion stores message metadata and attachment metadata, with
bodyHtmlinitiallynullandbodyStatusset topending. Drafts are skipped; Outlook messages in draft, junk, or trash folders are also skipped. - Replaying an ingested message updates its read state without duplicating the
email or its attachments. When
bodyStatusispending, ingestion queues a body-fetch job with up to five attempts. - A
messageUpdatedevent changes a known email's read state only when it contains a read-state change. For an unknown email, Korint fetches its current metadata and runs ingestion, including the account and folder checks. A message that no longer exists at the provider is ignored. - Inactive or deleted accounts skip new-message and message-update processing.
A
messageDeletedevent leaves the stored email in Korint.
How message bodies become available
The internal FETCH_EMAIL_BODY command uses fetchMessageBody to retrieve the
message without marking it as read. It stores EmailEngine's web-safe HTML in
Email.bodyHtml, without embedding attached images. If only plain text is
available, Korint escapes HTML characters and wraps the text in <pre> tags.
A message with no body stores an empty string, which counts as a completed
retrieval.
bodyStatus | Meaning |
|---|---|
pending | The body is awaiting retrieval or a retry. |
stored | Retrieval succeeded, including an empty body. Stored bodies are not fetched again. |
unavailable | The provider returned HTTP 404: the message is gone. The job completes without retrying. |
failed | Retrieval failed on the last known attempt. |
Other retrieval errors are recorded in bodyError and propagated for retry;
bodyHtml remains null. Without retry metadata, an error is not classified as
the final attempt. A successful retrieval clears bodyError.
Message responses larger than 10 MiB are rejected through the same error path. Korint does not truncate the response or save a partial body: truncating markup before sanitization can turn a non-empty message into an empty body.
General inbox
The Emails → Boîte de réception page lists conversations (threads), not individual emails. A thread is the set of emails of one account sharing the provider's thread id, or a single email when the provider gives none; the same conversation received by two connected accounts makes two threads. Nothing is stored per thread: the list is computed from the emails when it is read.
GET /email-threads returns the threads to process by default
(mode=to_process): those with at least one email that is not archived and has
no active linked row in EmailEntityLink. mode=processed returns the
complement, threads whose every email is archived or linked, so a thread is in
exactly one mode. In processed mode only, processedFilter=linked keeps threads
with at least one linked email and processedFilter=archived those with at
least one archived email; a thread with both appears under both. Sending
processedFilter in the other mode is a 400. unread=true keeps threads with
at least one unread email that is not archived.
Threads come newest first, ordered by their latest email, and page through an
opaque nextCursor. accountIds (repeatable) narrows the list to some
mailboxes; omitted, it covers every mailbox the caller may list, and ids outside
that set are ignored.
Every broker sees the threads of their own personal accounts. Brokers with the shared-account admin permission also see the firm's shared accounts. Colleagues' personal accounts are never listed. Disconnected accounts keep their threads.
Each thread shows its latest email (counterpart, subject, excerpt, date,
attachment flag), whether any of its non-archived emails is unread, toProcessCount,
isArchived (every email archived), and the
policies linked to any of its emails with their policy number. Its id is
opaque and never changes: it encodes the account and the thread key, since the
same thread key can exist in two accounts. Listing never marks an email as read.
Archiving a thread or an email
Archiving is a broker decision only: linking an email to a policy, automatically
or manually, never writes archivedAt. PATCH /email-threads/:threadId with
{ "archived": true } archives every email of the thread; adding emailIds
narrows it to those emails, and ids outside the thread are ignored. false
unarchives the same way. Archiving an archived email keeps its first
archivedAt. The response lists the emailIds whose state changed, which is
what the inbox's "Annuler" toast sends back with the opposite value.
A broker archives in their own personal accounts; the firm's shared accounts
require the shared-account admin permission. A malformed, unknown or invisible
thread id answers 400. Archiving is a Korint state only: nothing is written to
the mailbox. A new email arrives unarchived, so it brings its thread back to
process.
GET /general-inbox/summary returns the sidebar badge, unreadThreadCount: the
threads to process with at least one unread, non-archived email, over the same mailboxes. Like the other endpoints of this capability, both
are internal and not part of the public partner API.
Reading an email
GET /emails/:emailId returns one stored email for the general inbox: its
addresses, bodyStatus and bodyHtml, and its attachments with their storage
status. The caller must be an authenticated broker, and the email must belong to
one of the broker's own accounts or to an account shared within the broker's
brokerage firm, with or without the shared-account admin permission; only the
inbox list reserves shared accounts to admins. Disconnected, errored and deleted
accounts keep their stored emails readable. An unknown or invisible email answers
HTTP 400, so the response does not reveal whether the email exists.
Opening an unread email marks it read in Korint and queues an
email.mark_as_seen job, five attempts with exponential backoff, that adds the
\Seen flag through EmailEngine; a message that no longer exists at the
provider counts as done. The job is queued before the local flag is written: a
failed request can leave a mailbox write without a local flip, which the next
messageUpdated event repairs, but never a local flip without a mailbox write.
There is no "mark as unread"; the mailbox remains the source of truth and later
flag changes are mirrored by the synchronization described above.
GET /email-threads/:threadId opens a whole thread: every email, oldest first,
with its archive state, whether it is still to process (same rule as
toProcessCount) and its linked policies, but no body. It uses the same
visibility as GET /emails/:emailId, answers 400 for a malformed, unknown or
invisible thread id, and marks every unread email of the thread read with one
email.mark_as_seen job each, in the same order. The frontend then reads each
body through GET /emails/:emailId as the email scrolls into view.
Each attachment carries status (pending, stored, failed, skipped) and
failureReason. A stored inline attachment also carries inlineUrl, a presigned
URL valid for five minutes that the frontend substitutes for the matching cid:
reference in the body. Regular attachments are downloaded through
GET /emails/:emailId/attachments/:emailAttachmentId/download-url. Like the
other endpoints of this capability, it is internal and not part of the public
partner API.
Policy exchanges
The policy dashboard's Échanges tab shows whole conversations as soon as one email has an active link to the policy. Every broker with policy access sees their own mailboxes and the firm's shared mailboxes, including disconnected history. Colleagues' personal mailboxes remain private.
The tab loads GET /email-threads?policyId=…, including the metadata for every
email, without marking anything read. Conversations appear newest first; emails
within each conversation appear oldest first. An icon identifies emails that are
not linked to this policy, and each row shows its mailbox connection status.
All emails start collapsed. You can expand several independently: each expansion
reads that email through GET /emails/:emailId, leaving the others' read state
unchanged. Link reasons remain stored for traceability and are not displayed.
An expanded email lists its linked policies and lets the broker edit them: attach the email to a policy, detach it, or move a link to another policy. Each edit targets that single email through the manual link commands below; its other links are kept, and the conversation leaves the tab once no email remains linked to the policy.
How emails are linked to policies
Manual decisions apply to individual emails. Analysis requests cover a thread
within one connected account. When the body job reaches a
terminal bodyStatus (stored, unavailable or failed), the same
transaction records an analysis request for the message's thread in
EmailThreadAnalysis. The thread key is the provider's thread id, or the email
id when the provider gives none. Each request increments requestedVersion; a
future linking run must acknowledge only the version it read, so a message that
arrives during a run remains pending. Replaying an already stored body creates a
missing request without fetching the body again.
The internal email commands let you attach one email to several policies, detach one policy, or move a link between two policies atomically. Each command checks email visibility and access to every policy before changing links. A move requires an existing source link and a different target policy. None of these commands changes the email's archive or read state.
Decisions and their thread-analysis request commit together. A successful command confirms the individual email's manual decisions; propagation to later messages runs asynchronously. There is no undo command.
Decisions are stored in EmailEntityLink, one row per email, entity and decision:
status | Meaning |
|---|---|
candidate | Evidence for a link was observed, but the entity is not attached. |
linked | The entity is attached to the email. |
detached | The policy is manually excluded from the email. |
reasons stores the observed evidence; manual commands retain each pair's reasons.
A status change soft-deletes the active row and inserts a new one, so an email
has at most one active row per entity and its history stays queryable.
decidedByUserId and decidedAt identify a manual decision. Manual decisions
always take precedence: the linking run never overwrites them,
including when a broker corrects a link while analysis is running.
Automatic policy attachment
Analysis processes the whole conversation within one connected account, from the oldest email to the newest. Without a provider conversation identifier, each email is its own conversation. Body download completion and manual link corrections request another pass; reading an email does not.
The first matching rule supplies every eligible policy:
- Policies observed on earlier emails, including their active manual attachments.
- Current external policy references or contract numbers in the subject and full visible body, including forwarded text, quotations and signatures.
- Customer email addresses: From for received messages, To, Cc and Bcc for sent messages. The connected mailbox's own address is excluded.
Number matching ignores case but preserves punctuation, internal spaces and leading zeros. Attachment contents are not searched. Customer addresses come from configured email fields, for every customer role. Matching stays in the account owner's tenant and brokerage portfolio, including a wholesaler's retail firms. Quotes, subscribed and stopped policies are eligible; deleted policies are excluded. Quotes use their open new-business branches until subscription, then the current base policy.
A manual detachment excludes that policy from its email onward. Later observations
remain stored as candidate rows; exclusions never cause a fallback to a lower
matching rule. A later manual attachment lifts the exclusion from its own position
onward. Automatic attachment does not archive emails.
The inbox shows an analysis indicator while a requested pass is unacknowledged and has retries remaining. Exhausting the retries removes the indicator; a new analysis request displays it again. An email leaves “À traiter” once it has an active link. Analysis failures remain recorded for investigation; jobs retry up to eight attempts, and a periodic recovery sweep repairs interrupted handoffs without resetting an exhausted request's budget. An account without an owning brokerage firm records an analysis error and creates no automatic links.
Alerting the broker who connected the account
When a connected account stops syncing because its authorization has expired or
been revoked, EmailEngine reports an authenticationError and Korint marks the
account as error. On the transition into that state, the broker who connected the
account receives an email at their Korint login address inviting them to
reconnect the mailbox, with a link to the connected email accounts page.
The alert fires once per transition: EmailEngine re-emits the error on every
retry and webhook deliveries can be replayed, but a conditional write lets only
the job that actually flips the account to error send the email. The alert is
gated on the error status alone, so a disconnected account raises none:
EmailEngine reconnects a disconnected account on its own with backoff, so it
needs no broker action.
Like the rest of this capability, the alert is configured for the demo tenant only, and it is delivered through the internal SES transport.