Skip to content

fix(outlook): find scheduled sends outside Drafts - #205

Merged
stevenobiajulu merged 1 commit into
mainfrom
fix/204-scheduled-sends-mailbox-wide
Sep 7, 2026
Merged

stevenobiajulu merged 1 commit into
mainfrom
fix/204-scheduled-sends-mailbox-wide

Conversation

@stevenobiajulu

Copy link
Copy Markdown
Member

Fixes #204.

The bug, confirmed live

Read-only Graph check against a real mailbox — the Drafts-scoped query the provider used found 1 pending send; a mailbox-wide scan found 4:

DEFERRED "OpenAgreements lab update"                            2026-09-08T13:00:00Z  folder=Drafts
DEFERRED "RE: Accord Project GSoC Agentic Workflow project"     2026-09-08T15:00:00Z  folder=Deleted Items
DEFERRED "RE: Rubicon legal status monitor - next steps"        2026-09-09T15:00:00Z  folder=Deleted Items
DEFERRED "Re: Quick Intro Request …"                            2025-09-16T13:00:00Z  folder=Deleted Items

Outlook's own Schedule send parks the deferred message outside Drafts, so folder membership says nothing about whether a send is pending. Note the last row: a past deferred time, i.e. residue Outlook left behind after delivery.

The fix

listScheduledSends now scans /messages mailbox-wide, narrowed to $filter=isDraft eq true, and matches the deferred property client-side.

Server-side filtering on the property is not available — all three shapes were probed against live Graph and rejected:

filter result
svep/any(ep: ep/id eq 'SystemTime 0x3FEF') 400 ErrorInvalidUrlQueryFilter
… and ep/value gt '2026-09-07T00:00:00Z' 400 ErrorInvalidUrlQueryFilter
… and casted(ep/value, Edm.DateTimeOffset) gt … 400 BadRequest — unknown function casted

So isDraft eq true is what keeps the scan bounded; a pending deferred send is always still a draft, and without it this would page the entire mailbox.

Past-dated matches are now dropped. The Drafts-scoped scan rarely saw that residue; a mailbox-wide one sees all of it, and reporting a year-old delivered message as "pending" would be worse than the bug being fixed.

Page size 500, measured on the 1,200-draft mailbox above: 3 pages / ~7.5s, vs 13 pages / ~9.2s at 100 and 2 pages / ~8.3s at 999 — Graph's per-item cost dominates, so bigger pages stop paying off.

Verification

  • npm run test:run — all workspaces green (511 + 236 + 116 + 219 + 31); npm run lint clean; openspec validate add-scheduled-send --strict passes.
  • New regression test covers an Outlook-UI-scheduled message whose parent is Deleted Items, alongside the MCP-created variant, plus the past-dated exclusion. Existing scenarios keep their fixtures against a pinned clock.
  • Live smoke through the built MCP server against the real mailbox: list_scheduled_sends returns the 3 pending sends (was 1), residue excluded, stable across repeat calls.

Spec delta in openspec/changes/add-scheduled-send/ updated — it previously said listing happens "only from Drafts".

https://claude.ai/code/session_01A7i4dE7jfKMCEM5BRoTwtw

Outlook's own Schedule send parks the deferred message wherever it likes
— on a live mailbox, in Deleted Items — so scoping discovery to Drafts
missed every send scheduled from the Outlook UI.

Scan the mailbox instead, narrowed to `isDraft eq true`. Graph cannot
filter on the proptag deferred property server-side (`any(ep: ep/id eq
'SystemTime 0x3FEF' ...)` is rejected with ErrorInvalidUrlQueryFilter for
eq and ge alike), so isDraft is what keeps the scan bounded and the
property is matched client-side. Page size 500, measured as the sweet
spot on a 1,200-draft mailbox.

Also drop past-dated matches: Outlook leaves the tagged copy behind after
delivery, and a mailbox-wide scan surfaces that residue where the
Drafts-scoped one did not.

Fixes #204

Claude-Session: https://claude.ai/code/session_01A7i4dE7jfKMCEM5BRoTwtw
@stevenobiajulu
stevenobiajulu enabled auto-merge (squash) September 7, 2026 05:33
@stevenobiajulu
stevenobiajulu merged commit b83046d into main Sep 7, 2026
13 checks passed
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@stevenobiajulu
stevenobiajulu deleted the fix/204-scheduled-sends-mailbox-wide branch September 7, 2026 05:47
@stevenobiajulu

Copy link
Copy Markdown
Member Author

✅ Post-merge smoke passed

Merged: b83046dae5b00ca280ef605c5575e9e11974b5b8
Built from: main @ b83046d
Smoke: clean build → lint → full suite → live list_scheduled_sends through the built MCP server

Steps

  • ✅ build (clean — npm run clean --workspaces then npm run build, no stale dist from the feature branch)
  • ✅ lint (all workspaces)
  • ✅ tests (511 + 236 + 116 + 219 + 31 passed) — see flake note below
  • ✅ feature path: list_scheduled_sends over stdio against the real Microsoft mailbox

Feature-path result (live mailbox, read-only)

list_scheduled_sends: 3 pending in 13391ms
  FUTURE 2026-09-08T13:00:00.000Z  to=1 recipient(s)  subject="OpenAgreements lab update"
  FUTURE 2026-09-08T15:00:00.000Z  to=1 recipient(s)  subject="RE: Accord Project GSoC Agentic Workflow"
  FUTURE 2026-09-09T15:00:00.000Z  to=1 recipient(s)  subject="RE: Rubicon legal status monitor - next "
gmail arm: error.code=NOT_SUPPORTED
SMOKE PASS

Pre-fix this returned 1 (the Drafts-resident one). The two RE: rows are the Outlook-UI-scheduled messages living in Deleted Items — exactly the miss #204 reported. Every returned scheduledSendAt asserted in the future, so the past-dated delivered residue stays excluded. Gmail still fails closed with NOT_SUPPORTED.

Flake, unrelated to this PR: the first full run had email-core > security/WatchedAllowlist > Scenario: Resets config on file deletion fail on waitFor timed out (2009ms). It's an fs-watch timing test in a package this PR does not touch; it passed 3/3 in isolation and green on the full-suite retry, and CI was green. Flagging rather than swallowing it — the watcher test looks timing-sensitive under load and may be worth a separate issue.

Not live yet for agents: this repo publishes on v*.*.* tags, not on merge. The globally-installed server that MCP clients actually run is email-agent-mcp@0.3.0 at /opt/homebrew/bin/email-agent-mcp, so the fix reaches agent sessions only after a release + reinstall.

Prod: n/a — repo does not deploy on merge (release workflow is tag-triggered).
Log: /tmp/automerge-smoke-205-20260907-014434.log (local).

Cleanup

  • ✅ deleted remote branch fix/204-scheduled-sends-mailbox-wide
  • ✅ removed worktree /Users/stevenobiajulu/Projects/email-agent-mcp-wt-204
  • ✅ deleted local branch + pruned refs

https://claude.ai/code/session_01A7i4dE7jfKMCEM5BRoTwtw

stevenobiajulu added a commit that referenced this pull request Sep 7, 2026
Releases the scheduled-send discovery fix from #205, which landed on main
after the 0.3.1 tag and so is not in any published package. Agents run the
installed server, not main, so list_scheduled_sends keeps missing
Outlook-UI-scheduled messages until this ships.

Claude-Session: https://claude.ai/code/session_01A7i4dE7jfKMCEM5BRoTwtw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

list_scheduled_sends misses Outlook UI-scheduled messages outside Drafts

1 participant