# Connected email — feature definition

**Status:** draft for review · **Owner:** Sky · **Placement:** **phase 1, milestone M7** — after M6,
before **M8** migration and cutover (see §06)

This document defines the email feature in three layers, because one layer cannot do three jobs:

| Layer | What it is | Who argues with it |
|---|---|---|
| **§01–02 Description** | What the feature is, in business language. What it replaces and what it deliberately is not. | Sky, sales |
| **§03 User stories + acceptance criteria** | Given/When/Then, one set per story. Testable. | Sales, then QA |
| **`crm/prompts/M7-connected-email.md`** | The build prompt. Tables, columns, guards, screens, exit criteria. | Lovable |

The three are not alternatives. The stories are what make the build prompt testable; the build
prompt is what stops Lovable inventing table names, column types and RLS policies. **Every
acceptance criterion in §03 maps to a line in the prompt's Tests section** — that mapping is the
point, and it is checked in §05.

---

## 01 · What the feature is

**A rep's email lives in their mailbox and their work lives in ConnectIQ. This feature makes
those the same conversation without asking the rep to do anything twice.**

A rep connects their Google Workspace mailbox once. From then on, mail they exchange with people
ConnectIQ already knows about appears on that contact's record — sent and received, automatically,
without a BCC, a plugin, or a copy-paste. They can also write a new email from a contact,
company or opportunity record, from a shared template, and it sends **from their own mailbox** —
so it threads properly for the customer and appears in the rep's own Sent folder, exactly as if
they had written it in Gmail.

The thing being replaced is HubSpot's connected inbox. **DR-31 answered that this is not
optional** — a compose link and an outbound record was the cheap alternative, and the plan
records it as *the most likely cause of a rep saying the new system is worse than the old one*.

### The five mechanisms HubSpot bundles, and which ones we build

HubSpot's "email" is five separable things. Naming them separately is what makes this scopeable:

| # | Mechanism | v1? |
|---|---|---|
| 1 | **Connected inbox** — OAuth to the mailbox; read sender, recipients, subject, body | **Yes** |
| 2 | **Logging rules** — log mail from known contacts, plus a never-log list | **Yes** — the never-log list is the privacy control that makes 1 acceptable |
| 3 | **BCC / forwarding address** — logs with no connected inbox at all | **No** — deferred; see §04 |
| 4 | **Tracking** — one-pixel opens, wrapped-link clicks | **No** — deferred; see §04 |
| 5 | **Templates and snippets; sequences** | **Templates yes. Sequences no** — see §04 |

### The one thing HubSpot solved that we inherit

**Sending as a rep spends that rep's own provider quota.** Google caps daily sends per mailbox.
HubSpot layers its own limits — 500 a day, three a minute in bulk — on top, and that is not
politeness, it is self-defence: without a limiter of our own, the first bulk send throttles a
rep's real email account and they lose the ability to send personal mail. **The limiter is not
optional once sending exists.** It is in v1.

---

## 02 · What this feature is not

Four boundaries, each already settled somewhere else in the documentation set:

**It is not the transactional email layer.** ConnectIQ already sends system mail — user
invitations and quote approval requests — through `plat_email_queue` and its worker, via the
Resend HTTP API, from `notifications@connectiq.giant-pumpkin.com`. **That path does not change
and this feature does not touch it.** Anything sent *as a rep* goes through the rep's own
mailbox or it will not thread for the customer and will not appear in their Sent folder. Two
paths, two purposes, two send logs. Conflating them is the single easiest way to get this wrong.

**It is not a workflow engine.** A sequence — five scheduled emails per recipient, terminating on
a reply or a booked meeting, bulk enrolment, follow-up tasks — is not an email feature. It is
also gated on evidence we do not have yet: **the second of DR-31's two checks owed** asks how many
sequences exist in HubSpot today and how many enrolments happened in the last ninety days. **If
that number is near zero, sequences are not part of what is being replaced, whatever the licence
permits.** (The register calls this reasoning pattern *"the DR-08 test"* by analogy — DR-08 itself
is the closed site-package decision, not a sequence count. The open check belongs to DR-31.) Note
the dependency direction: exit-on-reply needs inbound capture, so the connected inbox is a
prerequisite for sequences rather than an alternative to them. Capture first, always.

**The timeline starts empty at connection — and connection is now earlier than cutover.** DR-27
decided email history does not migrate, and **that is untouched**: no backfill, no import, no admin
override. But capture runs from the moment a rep connects their mailbox, and shipping this before
cutover means they connect during M7 rather than on launch day. **Every week between connection and
cutover is a week of real correspondence already sitting on the record when reps arrive.** Make
connecting mailboxes an explicit cutover-prep step, as early in M7 as the feature allows — it is the
cheapest item in this plan and it converts the day-one experience from an empty panel into a
populated one. Reps still need telling that anything older stays in the HubSpot read-only archive
window, **before cutover, not after**.

**It cannot serve everyone, and that is a runtime fact rather than a choice.** HubSpot offers
three connection paths; ConnectIQ can offer two. Gmail API and Microsoft Graph are HTTP and work
on serverless. **IMAP needs a long-lived TCP connection, which the runtime forbids** — so
HubSpot's fallback for everyone else is not available to us. Taken for this build: **the team is
Google Workspace only**, so nothing is lost.

> **That is supportable but not formally closed.** It rests on the development strategy page
> referring to *"the Google Workspace account we already pay for"* and *"the Google Workspace
> records on `giant-pumpkin.com`"* — which is evidence, not a decision. **It is the first of
> DR-31's two checks owed and it should be closed explicitly**, because if it is wrong the people
> on another provider get no sync at all and mechanism 3 stops being a deferral and becomes their
> only option.

---

## 03 · User stories and acceptance criteria

Thirteen stories. Given/When/Then throughout. **Enforced** on a criterion means the database
enforces it, not the form — a criterion that only a disabled button protects is not met.

### E1 · Connect a mailbox

> **As a** sales rep, **I want** to connect my Google Workspace mailbox to ConnectIQ once,
> **so that** my customer correspondence records itself without me copying anything.

- **Given** I am an approved user with no mailbox connected, **when** I open Settings → Email,
  **then** I see one *Connect Google* action, a plain statement of what will be read, and a link
  to the never-log list.
- **Given** I start the connect flow, **when** Google asks for consent, **then** the scopes
  requested are **read and send only** — no mailbox modification, no delete, no label management.
- **Given** I complete consent, **when** I return to ConnectIQ, **then** my mailbox shows
  *Connected*, with the address, the connection time, and the point in time from which capture
  starts.
- **Given** I complete consent, **when** the connection row is written, **then** the refresh
  token is stored **encrypted outside the table** and is never readable by any client query, any
  admin screen, or any `rpt_` view. **Enforced.**
- **Given** capture has never run for my account, **when** the first sync completes, **then** it
  has looked no further back than the connection time — **it does not import history.** (DR-27.)
- **Given** another rep has connected a mailbox, **when** I open Settings → Email, **then** I see
  my own connection only, never theirs. **Enforced.**

### E2 · Disconnect, and survive a revoked token

> **As a** sales rep, **I want** to disconnect my mailbox and know what happens to what was
> already logged, **so that** connecting it does not feel irreversible.

- **Given** my mailbox is connected, **when** I disconnect it, **then** capture and sending stop
  immediately, and the stored token is destroyed rather than merely flagged.
- **Given** I disconnect, **when** I look at a contact I had corresponded with, **then**
  **already-logged messages remain** — disconnecting stops future capture, it is not a retraction.
  The screen says so before I confirm.
- **Given** I revoke ConnectIQ's access from my Google account instead, **when** the next sync
  attempt fails on an invalid grant, **then** my connection moves to *Reauthentication required*,
  sync stops, and I see one prompt to reconnect. **The worker does not retry an invalid grant on a
  loop.**
- **Given** my connection is in *Reauthentication required*, **when** I try to send from a record,
  **then** sending is refused with the reason and the reconnect action, and **no message is
  silently queued for later**.

### E3 · The never-log list

> **As an** administrator, **I want** addresses and domains that are never logged, **so that**
> connecting a personal-and-work mailbox is acceptable to the person doing it.

- **Given** I am an admin in Settings → Email, **when** I add an entry, **then** I can add a full
  address (`someone@example.com`) or a whole domain (`example.com`), and it applies **to every
  connected mailbox in the organisation**.
- **Given** I am a rep, **when** I open my own email settings, **then** I can add entries that
  apply **to my mailbox only**, and I can **read but not edit** the organisation-wide list — a
  privacy control nobody can inspect is not one.
- **Given** a message has any participant matching a never-log entry — in From, To, or Cc —
  **when** capture evaluates it, **then** **no row is written at all**: not the body, not the
  subject, not the participant addresses. **Enforced. Suppression happens before the first write,
  never as a filter on read.**
- **Given** an entry for the domain `example.com`, **when** a message from
  `anyone@mail.example.com` is evaluated, **then** it is suppressed — **sub-domains match.**
- **Given** matching is evaluated, **when** the address differs only in letter case, **then** it
  still matches.
- **Given** I add a never-log entry today, **when** messages matching it were logged last week,
  **then** I am told plainly that the entry **is not retroactive**, and offered a separate action
  to delete what already matched.

### E4 · Automatic capture

> **As a** sales rep, **I want** mail I exchange with known contacts to appear in ConnectIQ by
> itself, **so that** the record is complete without discipline.

- **Given** my mailbox is connected, **when** I send a message from Gmail to an address held on a
  ConnectIQ contact, **then** within one sync interval that message is on that contact's record,
  marked outbound.
- **Given** the same, **when** that contact replies, **then** the reply is on the record, marked
  inbound, and **attached to the same thread as the original**.
- **Given** a message whose participants match **no** ConnectIQ contact, **when** capture
  evaluates it, **then** it is not logged. Capture is *log mail with known contacts*, not *log
  everything*.
- **Given** a message with several known contacts in To and Cc, **when** it is captured, **then**
  it is linked to **every** one of them, and appears on each of their records — one message, many
  links, not many copies.
- **Given** the sync worker processes the same provider message identifier twice, **when** it
  writes, **then** the result is exactly one message row and no duplicated links. **Enforced by a
  uniqueness constraint, not by a check-then-insert.**
- **Given** a message carries attachments, **when** it is captured, **then** the **file names,
  types and sizes** are recorded and shown, and the file contents are not stored. (Open: DR-50.)
- **Given** the worker has been unable to reach Google for several intervals, **when** I look at
  Settings → Email, **then** I see when my mailbox last synced successfully, and a warning if
  that is stale. **Silence is never presented as success.**

### E5 · The timeline on the record

> **As a** sales rep, **I want** correspondence on the contact, company and opportunity record,
> **so that** picking up a deal does not start with a search through Gmail.

- **Given** a contact with logged correspondence, **when** I open their record, **then** an Email
  tab lists messages newest first, showing direction, counterpart, subject, date and a snippet.
- **Given** that list, **when** I open one message, **then** I see the full body rendered safely —
  **remote images are not loaded and scripts are stripped.** A logged email is untrusted content.
- **Given** a company record, **when** I open its Email tab, **then** I see correspondence with
  **every contact at that company**, attributed to the contact it belongs to.
- **Given** a contact with no logged correspondence, **when** I open the Email tab, **then** I see
  a designed empty state that says capture starts from the connection date — **not a blank panel
  and not a spinner that never resolves.**
- **Given** a thread of several messages, **when** it appears in the timeline, **then** it reads
  as one thread rather than as unrelated rows.

### E6 · Send from a record

> **As a** sales rep, **I want** to write an email from the contact record and have it come from
> my own mailbox, **so that** the customer sees a normal email and I still have it in my Sent
> folder.

- **Given** a contact with an email address and my mailbox connected, **when** I choose *Email*
  on their record, **then** a compose panel opens with the recipient filled in.
- **Given** I send it, **when** delivery to Google succeeds, **then** the message appears **in my
  own Gmail Sent folder**, and is logged against the contact with no second sync required.
- **Given** a contact with **no** email address, **when** I choose *Email*, **then** the action is
  refused with that reason. **Enforced.**
- **Given** my mailbox is not connected, **when** I open a record, **then** the *Email* action
  tells me to connect first rather than failing after I have written the message.
- **Given** I send from an opportunity, **when** the message is logged, **then** it is linked to
  **both** the contact and that opportunity — not to the contact alone.
- **Given** the send fails at Google, **when** I am told, **then** the failure names the reason,
  the message body is **not lost**, and no partial log row implies it was sent. **A message is
  logged as sent only after Google has accepted it.**

### E7 · Reply in thread

> **As a** sales rep, **I want** to reply from ConnectIQ and have it thread for the customer,
> **so that** they are not looking at a new disconnected email.

- **Given** a logged inbound message, **when** I reply from ConnectIQ, **then** the outgoing
  message carries the **thread identifier and the in-reply-to and references headers**, so the
  customer's client threads it.
- **Given** I reply, **when** the reply is logged, **then** it inherits the record links of the
  message it answers, and appears in the same thread on the timeline.

### E8 · Templates

> **As a** sales manager, **I want** shared email templates with placeholders, **so that** the
> team sends the same thing without retyping it.

- **Given** I can manage templates, **when** I create one, **then** I set a name, a subject, a
  body, and whether it is **private to me or shared with the team**.
- **Given** a template containing placeholders for contact first name, company name and my own
  name, **when** I insert it while composing to a contact, **then** each placeholder is replaced
  from the live record.
- **Given** a placeholder whose source field is empty on this contact, **when** I insert the
  template, **then** I am **shown which placeholders did not resolve before I send** — never a
  literal `{{first_name}}` reaching a customer. **Enforced: send is refused while an unresolved
  placeholder remains in the body.**
- **Given** I insert a template, **when** it is in the compose panel, **then** I can edit it
  freely, and editing it does not change the template.

### E9 · Who can see what

> **As a** sales manager, **I want** to see the team's correspondence, **and as a** rep **I want**
> to see only mine, **so that** coaching is possible without turning every mailbox into a
> notice board.

- **Given** I am a rep, **when** I open any record, **then** I see correspondence **from my own
  mailbox only**, whoever else has written to that contact. **Enforced at the database.**
- **Given** I am a sales manager or an admin, **when** I open any record, **then** I see all
  logged correspondence, attributed to the rep whose mailbox it came from.
- **Given** I am a rep, **when** I query the messages table directly with my own credentials,
  **then** another rep's messages are **not returned**. Row-level security, not a hidden tab.
- **Given** I am a rep who holds `view_all_records` but **not** the email flag, **when** I open a
  record, **then** I still see **only my own** correspondence. **These are two separate
  permissions** — §15 gives *see everyone's records* a flag path for users and *see others' logged
  email* a flat **No**, and DR-26 expects the records flag to be on for most people. Resolving
  email through it would show nearly everyone nearly every mailbox.
- **Given** permission resolution, **when** a role has no explicit grant, **then** access is
  **not** granted — including for admins. There are no implicit defaults.

### E10 · Protect the rep's own mailbox

> **As a** sales rep, **I want** ConnectIQ never to exhaust my personal sending quota, **so that**
> using the CRM cannot cost me the ability to send email.

- **Given** a configured daily cap per mailbox, **when** I have reached it, **then** further sends
  are **deferred rather than dropped**, I am told clearly, and nothing is lost.
- **Given** a configured per-minute cap, **when** several sends are triggered in quick succession,
  **then** they are paced beneath it rather than issued in a burst.
- **Given** Google rejects a send for rate limiting, **when** the worker handles it, **then** it
  backs off and retries with increasing delay, and **stops after a bounded number of attempts**
  rather than retrying forever.
- **Given** any send through my mailbox, **when** it completes or fails, **then** there is a log
  row recording which mailbox, when, to whom, and the outcome — **separate from the Resend system
  send log.** The two are never merged.

> **There is a second reason for the limiter, and it is the company's rather than the rep's.**
> Resend was chosen partly so that CRM mail would not share sending reputation with the team's real
> correspondence — the strategy page is explicit that *"a run of bounced quote emails degrades
> deliverability for the humans, and there is account-suspension risk on top"*, and that putting
> Resend's records on the `connectiq.` subdomain means *"the Google Workspace records on
> `giant-pumpkin.com` are never touched"*. **Sending as the rep deliberately routes CRM-driven mail
> back onto the root domain.** That is inherent to the feature and worth accepting knowingly.

### E11 · Association to the deal

> **As a** sales manager, **I want** correspondence attached to the right opportunity, **so that**
> the deal record explains itself.

- **Given** a captured message whose contact is linked to exactly one open opportunity, **when**
  it is associated, **then** it is linked to that opportunity automatically.
- **Given** a contact linked to several open opportunities, **when** a message is captured, **then**
  it is linked to the **contact and company only**, and a rep can attach it to an opportunity by
  hand. **It does not guess.**
- **Given** a message linked to the wrong opportunity, **when** I detach it, **then** the link is
  removed and the message stays logged against the contact.

### E12 · Sync health, visible to an admin

> **As an** administrator, **I want** to see which mailboxes are connected and healthy, **so that**
> a silently broken connection is not discovered by a rep noticing a gap weeks later.

- **Given** I am an admin, **when** I open Settings → Email, **then** I see every user's
  connection status, last successful sync and error state — **and never the content of anyone's
  mail, and never a token.** Metadata only. **Enforced.**
- **Given** a mailbox has not synced successfully beyond a threshold, **when** I view the list,
  **then** it is flagged, and the flag is not conveyed by colour alone.

### E13 · The record still knows when it was last touched

> **As a** sales manager, **I want** a contact's *last contacted* date to move when a rep emails
> them, **so that** the CRM does not report someone as neglected the day after they were written to.

- **Given** a contact whose *date of last contact* is three weeks old, **when** a rep emails them
  and the message is captured, **then** that date moves to the message date.
- **Given** the same, **when** the contact replies, **then** the date moves again.
- **Given** a message suppressed by the never-log list, **when** capture skips it, **then** the date
  **does not** move — nothing was recorded, so nothing is inferred from it.

> **This story is why DR-54 exists.** M2 derives that date *"from activities, not entered"*, and
> this feature builds a message store beside activities rather than inside them. **If the two are
> not reconciled, E13 fails silently** — no error, no empty state, just a date that quietly stops
> being true. It is the least visible and most damaging defect in this specification.

---

## 04 · Deferred, with the gate that brings each one back

Deferral without a trigger is just an omission. Each of these has one:

| Deferred | Brought back by |
|---|---|
| **Open and click tracking** (pixel, wrapped links) | Reps saying they used it in HubSpot and miss it. Cheap to build, but adds a public endpoint, a click-redirect service, and a consent question that has not been asked. |
| **Sequences** | The DR-08 test: ninety days of HubSpot sequence usage above roughly zero. **Then it is its own milestone, not an extension of this one.** |
| **BCC / forwarding address logging** | Anyone joining who is not on Google Workspace, or a rep refusing OAuth on a mailbox that is also personal. |
| **Microsoft 365 / Graph connector** | The team ceasing to be Google-only. |
| **Attachment file storage** | DR-50. Metadata is v1; storing the bytes is a storage-cost and data-retention decision. |
| **Email history migration** | Nothing. **DR-27 is closed** — it does not come back. |
| **Calendar sync** | Out of scope here and already listed as deferred in M3. |
| **Shared team mailbox capture** | A support or sales alias becoming a real channel. |

---

## 05 · Acceptance criteria → tests

The build prompt's Tests section is not a separate list. It is these criteria, grouped by what
actually breaks:

| Test | Covers |
|---|---|
| Never-log matching, as a standalone module against fixtures | E3 — address, domain, sub-domain, mixed case, participant in From / To / Cc |
| Capture idempotency: same provider message applied twice | E4 — one row, no duplicate links |
| Auto-association: one open opportunity vs several | E11 — links when unambiguous, declines when not |
| Permission matrix per role × own/other mailbox | E9 — **including a rep holding `view_all_records` but not the email flag**, and the no-implicit-default rule |
| Cross-prefix timeline view returns nothing extra to an unprivileged rep | E9 — the opportunity tab reads a view, and a view built without `security_invoker` bypasses every policy beneath it |
| Rate limiter: cap boundary, burst pacing, backoff termination | E10 — deferred not dropped |
| Token lifecycle: refresh, invalid grant, reauth transition | E1, E2 — and no retry loop |
| Thread stitching across inbound, outbound and reply | E4, E7 |
| Placeholder resolution, including the unresolved case | E8 — send refused rather than a literal token sent |
| Body rendering: script stripped, remote image not loaded | E5 |
| Send failure: body preserved, no false sent row | E6 |
| Last-contact-date moves on send and on reply | E13 — per whichever option DR-54 chooses |

**Hand-written, not generated.** Per the quality bar, generated tests prove the application still
works; hand-written tests prove the behaviour is right, and only the second kind blocks a merge on
its own. **The never-log module and the permission matrix are the two that must never be
generated-only** — they are the privacy commitments this feature is allowed to exist on.

---

## 06 · Decisions this needs — and what the phase 1 placement settles

### The placement, stated plainly

**This is M7 — inserted after M6 reporting and before migration, which moved migration and cutover
from M7 to M8. Phase 1 is now eight milestones.** The placement agrees with the documentation set
rather than reversing it.

- `crm/` **DR-31**: *"Email becomes its own milestone and phase 1 gets longer. It does not get
  squeezed into M3."* **Followed exactly** — its own milestone, its own prompt, not folded into M3.
- `phase-2/` **§04**: *"Inbox and calendar sync — a CRM concern, not a deployment one. If it is
  wanted, **it belongs in phase 1 as its own milestone, not parked here**."* **Satisfied verbatim.**
- **DR-35** — phase 2 begins strictly after cutover, S1 the only safe overlap — **stands unamended.**
  Nothing here touches phase 2.

**The cost, accepted deliberately: phase 1 gets longer and the cutover date moves.** That is the
trade DR-31 already named and priced. What it buys is the risk the plan logs as *"sales resist the
switch because something they relied on is missing — most likely email logging"*. **Reps go through
cutover with email logging working.** That was DR-31's point and it is true again.

**The convention problem disappears entirely.** Phase 1 milestones carry acceptance criteria and
exit criteria as a matter of course, so the four breaks a phase 2 placement caused — S0's *"no
milestones, estimates or acceptance criteria"* line, the phase 2 page header's *"nothing here is
binding on a build session"*, the deployment-only scope line, and the three stale counts — **all
evaporate.** The phase 2 page needs no exception note and no amendment. It reverts to *"these five
prompts"*, *"§03 S1–S5"*, *"§05 five open decisions"*, and DR-47–DR-54 do not appear on it.

### The hazard this placement introduces — close it or admit it

**Last before cutover is the position most likely to be squeezed.** Every slip in M1–M6 lands on
M7, and the pressure at that point will be to start M8 without it. That is precisely the outcome DR-31 was
written to prevent, reached by a different route: the milestone would not be *squeezed into M3*, it
would be *squeezed out of phase 1*.

**Close it with a gate rather than with intent.** Either M7's exit criterion **is** the cutover gate
— cutover does not happen until a rep can connect a mailbox and see a threaded reply on a record —
or the placement is decorative. **DR-47 must say which.** If it is not a gate, then a phase 2
placement is the honest description of what will actually happen, and this document should say so.

### What the move puts on, and takes off, the critical path

**On: the Google restricted-scope question.** It was calendar time ahead of a phase 2 stage. It is
now calendar time ahead of **the whole migration** — if an internal Workspace app does need a
third-party security assessment, that is weeks between the code being finished and a rep being able
to connect, and cutover waits behind it. **This is the largest schedule risk in phase 1, and it is
answerable today.** Confirm it in the Google Cloud console against the actual project **before the
phase 1 plan is committed**, not before M7 is scheduled.

**Off: the twelfth permission as a post-cutover migration.** `view_all_email` was an additive change
against a shipped M1. With both in phase 1, **fold it into M1's permission set as the twelfth from
the start** — same mechanism, one more row in §15, no migration against live data. If M1 has already
shipped, it stays additive and nothing is lost.

**Off: deciding activity ownership across a phase boundary.** DR-15 is a phase 1 decision, and DR-48
and DR-54 ride on it. Having all three inside phase 1 is what their own notes asked for; deciding
them with a phase 2 stage in mind was always the awkward part, and that awkwardness is gone.

**A cutover-mechanics note.** During the overlap both HubSpot and ConnectIQ read the same mailbox.
Both connections are read-and-send only and neither modifies the mailbox, so they coexist without
conflict. **Disconnect HubSpot's inbox at cutover, not before** — removing logging from the system
reps are still working in is a regression they feel the same day.

### New register entries

| ID | Decision | Needs |
|---|---|---|
| **DR-47** | **Email logging is its own phase-1 milestone — M7, after M6, before the M8 cutover.** Affirms DR-31 rather than reversing it; **DR-35 is untouched**. Record the accepted cost — phase 1 gets longer, the cutover date moves — and **answer explicitly whether M7 gates cutover**, because last-in-phase is the compression target. | Sky · **now** — it sets the cutover date |
| **DR-48** | **Which domain owns logged messages.** Rides on DR-15. Note that **DR-15's own recommendation is `com_activities`** — DR-31 is the evidence that flips it to `cl_`, so this is an override to record, not an agreement. Decide the two together. | Sky · with DR-15 · **before M7 starts** |
| **DR-49** | **Push or poll.** Google Pub/Sub push notifications, or scheduled incremental polling on a stored history cursor. Polling adds no external dependency and suits the existing worker pattern; push is closer to real time and adds a topic to operate and a watch to renew. **Recommended: poll.** | Sky · before M7 |
| **DR-50** | **Attachments — metadata or bytes.** v1 records name, type, size. Storing contents is a cost and retention decision. **Recommended: metadata only.** | Sky · before M7 |
| **DR-51** | **How much body do we store, and for how long.** Full body makes the timeline useful and makes ConnectIQ a store of customer correspondence with a retention obligation. Snippet-only is cheaper and much less useful. **Recommended: full body, with a stated retention period.** | Sky · before M7 |
| **DR-52** | **Whose consent.** Reps consent by connecting. **Customers do not** — and their mail is being stored in a system they do not know about. The never-log list covers the rep's privacy; this entry covers the counterparty's. **Now a pre-cutover question rather than a post-cutover one**, which is better: it is answered before any customer mail is stored, not after months of it. | Sky · before M7 |
| **DR-53** | **Provider message IDs on a domain table, against rule 10.** Rule 10 says external system IDs live only in `int_external_system_mappings`. `cl_email_messages.provider_message_id` and `cl_email_attachments.provider_attachment_id` break it. The exception is arguable — it is the natural key the idempotency constraint needs, and Google is not one of the migration systems rule 10 protects — but **the standing context says to flag a rule conflict rather than resolve it in a migration comment.** Grant the exception, or make the key a same-domain FK to the raw row. | Sky · before M7 |
| **DR-54** | **Does a captured email also create an activity?** M3 already models activities with an `email` type and its own four-way link table, and M2 derives *date of last contact* from activities. **If a logged email is not an activity, that date goes stale every time a rep emails someone** — a visible regression exactly where reps look. Three options in the build prompt; recommended is re-deriving the date from activities *and* messages. | Sky · with DR-15/DR-48 · **before a table is created** |

**Which page owns these.** The DR series is global and the numbers never move. **DR-47 to DR-54
belong on the `crm/` phase 1 page**, alongside DR-15, DR-26, DR-27, DR-31 and DR-40 — that is where
the milestone now lives. **They do not go on the phase 2 page**, and DR-35 there is left alone.

### DR-40 — and the sequencing that now resolves half of it

DR-40 — *does the system email customers, or only staff?* — is open and blocks M5. Its assumed answer
is *"staff only in phase 1 — the quote step produces a compose link and an outbound record, and the
human sends it from their own mailbox."* **That is mechanically this feature.**

**M7 ships after M5, so M5 builds its own compose link and keeps it.** That is the sequencing
decision, and it means **M7 does not supersede M5 on arrival**. What it does mean:

- **M7 builds the `com_quote_id` link column and no "send quote" action**, exactly as before. Until
  DR-40 is answered, customer-facing quote sending is not authorised from this panel.
- **A follow-on question exists and should be logged rather than discovered:** once M7 has landed,
  does M5's compose link retire in favour of this panel, or do two compose surfaces live side by
  side permanently? **Two compose panels in one product is a support burden and a divergence risk.**
  This is now a *within-phase-1* question, which makes it cheap to answer — but only if someone
  asks it.
- **Do not let M5's implementation drift toward this one speculatively.** M5 ships first; it should
  ship the cheap thing DR-40 assumed, not a preview of M7.

### Two facts to verify — the first before the phase 1 plan is committed

**These are additional to DR-31's two checks owed, not those.** Neither could be verified while
writing this — treat both as open:

1. **Whether Google requires a security assessment for the scopes we need.** Gmail read access is a
   restricted scope. Apps published **internally within a single Workspace organisation** are
   generally exempt from the external verification and third-party assessment that public apps face
   — but if that exemption does not apply here, it is **weeks of calendar time, not build time**,
   and it now sits **between phase 1 and cutover**. **Confirm in the Google Cloud console against
   the actual project before the phase 1 plan is committed.** This is the one item on this page that
   can move the cutover date without anyone writing a line of code.
2. **The real per-mailbox daily send limit for the team's Workspace edition**, and the Gmail API's
   per-user rate limit. The limiter in E10 needs numbers, and they should be read off Google's
   current documentation rather than inherited from HubSpot's 500-a-day.

### Existing entries this milestone depends on

- **DR-15** — which domain owns activities. **Blocking**; DR-48 and DR-54 both ride on it. Its own
  note already anticipates this feature's boundary problem: *"an `rpt_` view for any timeline that
  crosses a prefix."* The opportunity and quote Email tabs are exactly that. **Now in the same phase
  as the milestone that needs it.**
- **DR-27** — closed. Quotes migrate; email threads and notes do not. Constrains E1 and E5. **The
  phase 1 placement softens its sting without touching it**: capture starts at connection, and
  connection can happen weeks before cutover, so the record is not empty on day one. Reps must still
  be told that anything older stays in the HubSpot read-only archive window, for as long as that
  lasts.
- **DR-31** — **affirmed, not reversed.** Email is its own milestone and phase 1 gets longer.
- **DR-35** — phase 2 starts strictly after cutover, S1 the only safe overlap. **Untouched.**
- **DR-40** — see above. Open, blocks M5, and this feature is its assumed answer. M5 ships first.
- **DR-31's second check** — ninety days of HubSpot sequence usage. Not blocking for M7; it decides
  whether a *further* milestone exists.
- **DR-26** — *"the see everyone's records flag will be on for most people."* This is why email
  visibility needs its own flag rather than riding on `view_all_records` — and why that flag should
  be folded into M1's set rather than added later.
