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. 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.

  2. 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.

  3. 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

  4. 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

  5. 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

  6. 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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions