Skip to content

service-mailbox: inline-forward parsing for the Gmail, Outlook and Apple Mail "Forwarded message" blocks — the chained follow-up of #22871 (ruling Q3 C staged) #23215

Closed

Description

@objectstack-fleet

This card carries the inline-forward half of #22871's ingest seam (ruling Q3 C, staged). #22871 keeps the structured half: the outer headers plus the headers of a top-level message/rfc822 part.

Blocked-by: #22871

Part of #8998.

Filing gate ③, ordered by the maintainer's ruling 6116489638 on #22870 (batch #319 item 2, 「同意」), recorded on #22871 in 6116501990: "A chained follow-up card, Blocked-by: #22871, filed by the domain:services lane when #22871 is dispatched". Triage 6137138392 moved #22871 to the spec lane and named the seat that dispatches stage 1 as the filer. This card is that filing.

Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H, in the act that claims #22871 stage 1. ⛔ Not a claim. Reader who acts: triage, to grade (the domain is inherited from #22871); then the owning seat, once #22871 has landed.

Scope, from the ruling (⛔ nothing here re-decides it)

The ruling's text, verbatim: "(2) A chained follow-up card, Blocked-by: #22871, filed by the domain:services lane when #22871 is dispatched: inline-forward parsing for the "Forwarded message" blocks of Gmail, Outlook and Apple Mail only, one pin per client format; a block the parser does not recognise is not guessed, the message pools; only a forward sent by an organisation user is parsed (D5, "forwarding is consent" is the door, as Salesforce's acceptable-address list and HubSpot's own-mailbox rule are)."

  • What it reads: the "Forwarded message" block of the body for the three named client formats only.
  • What it writes: the parsed original participants join match_addresses with the source marker inline. Re-match replays the same input, and the attribution stays auditable (D2).
  • The door: only a forward sent by an organization user is parsed.
  • The miss: an unrecognized block is not guessed. The message pools.
  • Re-match stays additive (Q5 A). An explicit unlink is a later card.

Acceptance

  • One pin per client format (Gmail, Outlook, Apple Mail): a forwarded message in that format associates through the original sender's mailAssociation field. Its control is the same message without the block, which pools.
  • An unrecognized block pools, and nothing is guessed.
  • A forward from a sender who is not an organization user is not parsed, and the message pools.
  • The rows carry the inline marker in match_addresses.

Dedupe

The ruling orders exactly one such card. Search (MCP search_issues on this repo, closed included) for inline forward parsing Forwarded message block Gmail Outlook Apple Mail mailbox answered 1 hit: #22871 itself. No inline-forward card exists.

Dedupe words: inline forward parsing; Forwarded message block; match_addresses source marker inline; service-mailbox forwarding is consent.


Generated by Claude Code

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Oct 13, 2026
  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p2 · target:v18 · domain:spec, stays pm:blocked on #22871. A ruled follow-up

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T10:04Z. ⛔ Not a claim, ⛔ not a dispatch.

  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan: pm:blocked → pm:queue. Blocked-by: #22871 is satisfied: #22871 (connector-imap + minimal service-mailbox) closed completed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T00:57Z. ⛔ Not a claim, ⛔ not a dispatch.

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Oct 14, 2026
  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#23215, the inline-forward half of #22871's ingest seam, ruling 6116489638 Q3 C staged: Gmail, Outlook and Apple Mail "Forwarded message" blocks only, organization-user senders only, an unrecognized block pools; triage 6143150737, unlock 6158469549) · 2026-10-14T09:11Z
    Session: session_01YRp5z4kK6vgYkrdH97ztsy
    Account: huangyiirene (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23215-inline-forward-parse
    Worktree: objectstack-issue-23215
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main a7a135dc2 or later; stop on a breach and explain it in the report):


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 14, 2026
  7. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 23215,
     "status": "done",
     "branch": "claude/issue-23215-inline-forward-parse",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/23459",
     "session": "session_01YRp5z4kK6vgYkrdH97ztsy — subagent run: the parent PM's session id, as the dispatch, the claim 6164943484, every commit's Claude-Session trailer and the relay envelope carry it",
     "premise_still_valid": true,
     "summary": "service-mailbox's ingest now reads the 'Forwarded message' block Gmail, Outlook (on the web / Microsoft 365) and Apple Mail write into the text/plain body (new pure module src/ingest/inline-forward.ts, English labels only, the first separator decides, an incomplete or unknown block answers null and is never guessed) and appends its From/Reply-To/To/Cc to match_addresses under the declared source 'inline', after outer and attachment. H2 reading implemented: the door opens only when the outer From names exactly one address, a sys_user holds that email (ASCII case-insensitive, $icontains then exact on the value read back, bounded at SENDER_USER_SCAN_LIMIT=100) and that user has a sys_member row in the mailbox's organization; both reads are system reads scoped by the mailbox's organization, made only when a block was recognised; with sys_user/sys_member unregistered the door is closed and said once at warn. No new dependency, no new published contract, no new package-entry export (built dist/index.d.ts names none of parseInlineForward / INLINE_FORWARD_FORMATS / SENDER_USER_SCAN_LIMIT), so Clause-②: no stands. H1 measured: the set is built in mail-ingest-service.ts ingest() via collectMatchAddresses (outer, then attachment; inline now third). H3 measured: ParsedInboundMessage.text is the text/plain part and postal-mime 4.0.5 leaves it null for an HTML-only message (pinned), so an HTML-only forward pools; localized labels (Gmail's Von:, Outlook's and Apple Mail's localized labels) are not read and pool. H4 measured: rematch reads id/match_addresses/association_state only and replays the stored set; pinned that inline entries replay and the body is never re-read.",
     "tests": "All at head a870b3c82 (the PR head; origin/main merged in before the PR existed), each through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-23215, VERDICT command-exit 0: (1) pnpm --workspace-concurrency=2 --filter '@objectstack/service-mailbox...' build (package + upstream closure) -> BUILD_CLOSURE_AND_SELF_EXIT=0; (2) pnpm --filter @objectstack/service-mailbox typecheck -> TYPECHECK_EXIT=0 (tsconfig include src, tests reached; check:type-check-coverage exit 0); (3) pnpm --filter @objectstack/service-mailbox exec vitest run --maxWorkers=2 --reporter=verbose -> 'Test Files 9 passed (9) / Tests 132 passed (132)', TEST_EXIT=0: inline-forward.test.ts 24, mail-ingest-service.test.ts 27 (7 new), parse-message.test.ts 13, suppression 17, mailbox-service-plugin 16, mailbox-sync-service 26, objects-authoring-door 4, kernel tests mailbox-service-kernel 1 and mailbox-sync-kernel 4 (base 101 + 31 new). Real-engine pins (ObjectQL over better-sqlite3, real sys_user/sys_member registered): one per format (gmail/outlook/apple-mail) associates through mailtest_contact.email (mailAssociation) with stored set [rep@corp.test outer, customer-FORMAT@acme.test inline, rep@corp.test inline], control = same message without block pools with outer-only set; unrecognised Gmail 'Von:' block pools with no inline entry; door closed for no user / member of ORG_B only / two From addresses, control = same block from the org user associates; identity objects unregistered -> not read, warning said once over two messages; rematch replays inline and does not re-read body_text. Ablation (one, on the parser's call, at committed 0c49e3c8e): node scripts/ablation-replace.mjs replaced '    const forward = parseInlineForward(parsed.text);' with a constant null + marker; tool evidence 'anchor x1 -> x0', 'replace x0 -> x1', blob ecbf9870d513 -> 018d7b59ea60, in-run on-disk grep marker=1 anchor=0; direction RED as predicted: mail-ingest-service.test.ts 'Tests 6 failed | 21 passed (27)' (3 per-format pins, door control leg, said-once pin, rematch replay); restore proven by the tool ('blob == HEAD (ecbf9870d513) and git diff HEAD is empty') and re-checked after: disk blob ecbf9870d5 == HEAD blob, git diff HEAD 0 bytes, git status --porcelain empty, marker count 0. No dist leg: the subject is imported by relative path inside the package (src), not through an exports map.",
     "gates": "Local subset: node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 67 commands at a870b3c82 (identical list to 0c49e3c8e); each run with exit captured before any pipe (cmd > log 2>&1; status=$?), recorded as 'cmd :: exit N'. Run 1 at 0c49e3c8e: 65 exit 0, 2 exit 3 PREREQUISITE NOT MET (pnpm check:dual-build-cjs-loads, pnpm check:i18n: no workspace dist). Run 2 at a870b3c82: 67 of 67 exit 0 (check:dual-build-cjs-loads '110 published require entry point(s) across 68 package(s) load'; check:i18n 'check-i18n-bundles: OK (10 package(s) — all bundles in sync'). --ran reconciliation exit 0: '67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED'; it flags the tree 6 commits behind origin/main 7b3d5585c in four scripts/pm/fleet-write/* files (none of those commits touches service-mailbox; its closure gains only a spec refusal message), so PR CI on the merge ref is the reading for that range. CI (PR #23459 check-runs at a870b3c82, read at report time, not re-run locally): completed success — Check Documentation Links, Check PR Size, Dogfood Regression Gate, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; skipped — Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL); in_progress — Build Core, Check Changeset, Governed Surface Queue Guard, Flag docs affected by code changes, Lint · ESLint, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped (query-options erasure, slot lookup & corpus, tooling self-tests), Test Core (1/6 to 6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace; queued — Auto Label. Required set (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard) NOT MEASURED at report time, reason: in_progress; convergence is the PM's.",
     "mcp_calls": "0 — no MCP GitHub tool called (reads went through gh api REST GETs)",
     "api_writes": "2 relay strokes (each POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]) carrying 3 REST writes: POST /repos/objectstack-ai/objectstack/pulls (draft, run 38550807089, read-back 11037 bytes sent = stored), POST /repos/objectstack-ai/objectstack/issues/23459/assignees (huangyiirene), POST /repos/objectstack-ai/objectstack/issues/23215/comments (this os-dev-report). label-write: 0 (the dispatch names no labels and skip-changeset does not apply: the package publishes). git push x4 (empty-branch probe, two commits, the main merge) not REST.",
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: Version Packages PR #23382 (it consumes pending changesets into service-mailbox's CHANGELOG) · noted, not filed — the pending stage-1 changeset .changeset/22871-mail-ingest-seam.md (another card's) still says 'A forward quoted inline in the body is not read yet, and `inline` is declared so a later reader of it can store its addresses.'; if both changesets publish in one release the CHANGELOG carries that sentence beside this PR's entry. Fix if the seat wants it before release: delete that one sentence from 22871-mail-ingest-seam.md (not done here: editing another card's changeset turns Check Changeset red by design). Dedupe words: stage-1 changeset inline not read yet; mail-ingest-seam changeset sentence; service-mailbox CHANGELOG inline forward."
     ],
     "line_budget": "n/a (no skills/** in the diff)",
     "deviations": [
      "File surface: beyond src/ingest/** and the changeset, the diff touches packages/services/service-mailbox/README.md (its published sentence 'A forward quoted inline in the body is not read yet' became false with this change, so it is rewritten) and packages/services/service-mailbox/vitest.config.ts (source aliases for @objectstack/platform-objects/identity, @objectstack/spec/shared, @objectstack/spec/identity so the real-engine test registers the real sys_user/sys_member from source). Both declared in the PR body's Acceptance notes.",
      "Zone 3 route changed: the pure parser runs first and the organization-user door (two system reads) is read only when a block is recognised, instead of calling the parser only behind the door. Observable behaviour is identical (the parser has no effects; unrecognised or non-org forwards contribute nothing); it saves two reads on every message that carries no forward. Stated in the PR body.",
      "origin/main merged into the branch before the PR existed (0c49e3c8e -> a870b3c82; no conflicts, lockfile unchanged), then the closure rebuilt and the package suite and the 67 derived gates re-run on a870b3c82.",
      "Commit trailers: the model-free pair AGENTS.md prescribes (Claude-Session + Co-authored-by: Claude) instead of the harness reminder's model-named Co-Authored-By line, which AGENTS.md and the pre-push hook forbid; the main-merge commit carries git's default message without trailers. PR body footer is AGENTS.md's session-URL form, not the harness reminder's form.",
      "Format sources: the GitHub API read of crisp-oss/email-forward-parser answered 403 (repository not enabled for this session; add_repo not attempted). The fixtures were read from raw.githubusercontent.com (gmail_en_body.txt, apple_mail_en_body.txt, outlook_live_body.txt; outlook_live_en_body.txt answered 404) and the npm tarball email-forward-parser@1.8.3 (MIT), saved as untrusted data under scratchpad issue-23215/efp; only their shapes are reproduced in messages.fixture.ts with synthetic addresses.",
      "Scope bound, not a gap claim: English labels of the three formats only. Localized blocks, Outlook desktop's plain-text '-----Original Message-----' form and its no-separator HTML alternative, and HTML-only forwards are not read and pool; listed in the PR's Acceptance notes.",
      "Cleanup: worktree /home/user/objectstack-issue-23215 removed (node_modules deleted first; git worktree remove exited 0 without --force) after the PR opened and the branch head a870b3c82 was confirmed on origin. No process left running; scratchpad files stay under scratchpad/issue-23215/."
     ],
     "files_changed": [
      ".changeset/23215-inline-forward-parse.md (new)",
      "packages/services/service-mailbox/README.md",
      "packages/services/service-mailbox/src/ingest/inline-forward.ts (new)",
      "packages/services/service-mailbox/src/ingest/inline-forward.test.ts (new)",
      "packages/services/service-mailbox/src/ingest/mail-ingest-service.ts",
      "packages/services/service-mailbox/src/ingest/mail-ingest-service.test.ts",
      "packages/services/service-mailbox/src/ingest/match-addresses.ts",
      "packages/services/service-mailbox/src/ingest/messages.fixture.ts",
      "packages/services/service-mailbox/src/ingest/parse-message.ts",
      "packages/services/service-mailbox/src/ingest/parse-message.test.ts",
      "packages/services/service-mailbox/vitest.config.ts"
     ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23459 comment 6166527888 · domain:spec seat 1 (#6017) · session session_01YRp5z4kK6vgYkrdH97ztsy · 2026-10-14T10:42Z


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23459 → b60eb78d3 (Fixes #23215) · 2026-10-14T11:16Z

    domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy · holder of claim 6164943484. Record: ACCEPT ## 席位核对 6166527888 on a870b3c82. No contract face, so no contract review was owed.

    Reading Probe Answer
    The PR GET /pulls/23459 merged: true at 11:16:16Z, merge_commit_sha b60eb78d3
    The queue branch GET /branches, gh-readonly-queue/* none left: the merge group was consumed
    origin/main git merge-base --is-ancestor b60eb78d3 origin/main yes. b60eb78d3 is the tip, a squash on 8d5b7bb94
    The card GET /issues/23215 closed / completed on the merge. The PR's timeline cross-references only #23215

    State change: this act removes pm:dispatched. The seat record's out-of-scope note about stage 1's "not read yet" sentence is moot. That changeset shipped in 18.0.0-next.1 (Version Packages PR #23382), and this entry ships in a later version.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions