Skip to main content

Connected email accounts

This capability is available for demo purposes only. Its navigation entry is feature-flagged and hidden in the production environment.

Brokers can connect their email accounts (Gmail or Outlook) to Korint. There is no dedicated page: accounts are managed from the general inbox (Emails → Boîte de réception), in a side drawer that opens over the inbox, so closing it returns the broker to where they were.

The mailbox selector at the top of the inbox lists the accounts with their status and unread count, and ends with two actions: Connecter une boîte opens the drawer on the connection funnel, Gérer les comptes… opens it on the account list. When the broker has no account at all, the inbox shows an empty state with the same Connecter une boîte button.


What a broker sees​

The drawer requires an authenticated broker within a tenant, and it lists up to two groups, each shown only when it has accounts:

  • Personnelles: the broker's personal accounts (isShared: false).
  • Partagées: the brokerage firm's shared accounts (isShared: true), including those connected by the logged-in broker. Shared accounts belong to the firm; each card shows who last connected the account.

The groups use the sharing mode, not the identity of the person who connected the account. Within each group, accounts are ordered by connection date, oldest first. Every card shows the provider logo, the email address and a status indicator for the connection health.

Connect and manage accounts​

Connecter une boîte first asks who will see the mailbox: Personnelle or Partagée. Brokers without the shared-account management permission skip this question and connect a personal mailbox. The next step lists the providers: Gmail and Outlook, plus Boîte partagée Microsoft 365 for a shared connection only. Choosing Gmail or Outlook sends the broker to the provider; the outcome shows as a toast when they come back to the inbox.

A Microsoft 365 shared mailbox has no login of its own, so this 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. Reconnecting such an account repeats the same delegated sign-in.

You can disconnect your personal accounts from the card's ⋯ menu, and reconnect them from the card 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 connect it again with the other type. 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 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 messageNew event 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 bodyHtml initially null and bodyStatus set to pending. 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 bodyStatus is pending, ingestion queues a body-fetch job with up to five attempts.
  • A messageUpdated event 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 messageDeleted event 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.

bodyStatusMeaning
pendingThe body is awaiting retrieval or a retry.
storedRetrieval succeeded, including an empty body. Stored bodies are not fetched again.
unavailableThe provider returned HTTP 404: the message is gone. The job completes without retrying.
failedRetrieval 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 threads or emails​

Archiving is a broker decision only: linking an email to a policy, automatically or manually, never writes archivedAt. PATCH /email-threads with { "threadIds": [...], "archived": true } archives every email of those threads in one write (up to 500 threads); adding emailIds narrows it to those emails, and ids outside the threads 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. One malformed, unknown or invisible thread id rejects the whole request with 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. search keeps the threads with an email whose subject, participant name or address, or body text contains the term, case-insensitively. Accents are not folded and only common HTML entities are decoded. It requires policyId; without it the call is a 400.

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.

Two batch commands serve the inbox selection. POST /email-threads/policies links every email of up to 500 threads still to attach (not archived, no active link, including a manual exclusion) and returns their ids; POST /emails/policies/detach records an exclusion for each email and policy pair, which is how the inbox undoes a batch attach. Each runs in one transaction that locks the affected threads in a fixed order.

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:

statusMeaning
candidateEvidence for a link was observed, but the entity is not attached.
linkedThe entity is attached to the email.
detachedThe 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:

  1. Policies observed on earlier emails, including their active manual attachments.
  2. Current external policy references or contract numbers in the subject and full visible body, including forwarded text, quotations and signatures.
  3. 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 general inbox.

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.