Outlook inbox list strategies

Use canonical governed task ids (outlook_*). List harvest must not click inbox rows (no list_row clicks) — read-state mutation is limited to the conversation open task; see OUTLOOK-READ-STRATEGIES.md.

Updated

Quick decision

Inbox list strategy router

Pick a governed, read-only list task from Graph REST or DOM harvest based on session and scale.

  1. Need inbox message list / folder counts?Entry decision for harvesting inbox rows or folder counts.
  2. M365 cookies on Playwright page? → outlook_list_inbox_rest (or outlook_list_inbox_chunk list_strategy=auto)Prefer Graph REST when the Playwright page carries M365 session cookies.
  3. REST blocked, inbox loaded? → outlook_list_inbox_chunk (DOM harvest_list_rows + checkpoint)Fall back to DOM chunk harvest when Graph REST is blocked but OWA inbox is loaded.
  4. Need more rows than one chunk? → outlook_list_messages_scroll (DOM scroll rounds)Scroll the inbox DOM when a single chunk cannot collect enough rows.
  5. Smoke / visible rows only? → outlook_list_messages_viewportRead viewport-visible rows only for quick connectivity smoke checks.
  6. outlook_probe_mail_health (+ optional REST folder stats → folder_total_count, unread_count)Probe mail health before listing to confirm OWA is ready.

Governed tasks (list population)

task_id Produces Mutates OWA read? Typical use
outlook_list_inbox_rest items[], extra.via=outlook_rest, folder_total_count No Accurate counts + is_unread when Graph session works
outlook_list_inbox_chunk items[], next_cursor, chunk.has_more No REST fallback, resume checkpoints
outlook_list_messages_scroll items[] No Large folder DOM backfill
outlook_list_messages_viewport items[] No Health smoke
outlook_read_conversation_thread bodies Yes (list_row click) Open pass only; use preserve_unread

Strategy router (list_strategy)

outlook_list_inbox_chunk accepts query.list_strategy:

Value Behavior
auto (default) Try Graph REST once; on auth/403/parse failure → DOM chunk with ConnectorWarning list_strategy_fallback
rest REST only (fail if unavailable)
dom DOM harvest_list_rows only

Environment: FORGE_LCDL_OUTLOOK_LIST_STRATEGY (auto | rest | dom).

REST page size: FORGE_LCDL_OUTLOOK_REST_PAGE_SIZE (default 100, max 200). Each chunk may follow @odata.nextLink until chunk_size rows are collected.

Cockpit passes COCKPIT_OUTLOOK_LIST_STRATEGY into the bridge (same values). Guided harvest defaults: 100 rows per chunk request, 1000 messages per pass (max_messages).

Count signals (do not confuse)

Field Source Meaning
folder_total_count Graph inbox folder stats (REST) Mailbox size in folder
unread_count Graph (REST) Unread in folder
list_row_count_estimate DOM probe Rows visible in viewport now
seen (consumer) Rows processed in this sync run Not folder size

Output shape (REST items)

REST list items set source_kind to mail_rest, extra.is_unread from Graph isRead, and stable item_id from message id. DOM chunk items use extra.list_row + extra.is_unread from evaluate heuristics.

Pagination: REST uses @odata.nextLink stored in checkpoint rest_next_link; DOM uses scroll position + seen_item_ids.

SharePoint pattern

SharePoint uses browser-session page.context.request.get with SharePoint cookies. OWA on Edge often does not attach a Graph bearer to Playwright context.request (401 Access token is empty); list_strategy=auto then falls back to DOM harvest_list_rows + scroll until up to chunk_size rows (default 100) per chunk. When Graph works (some tenants/profiles), REST is used automatically.

DOM list harvest and tab visibility

DOM harvest_list_rows and search-day navigation (received:M/D/YYYY in the OWA search box) only populate rows when the tab reports visible. A background Edge tab (document.hidden === true) often leaves the search query typed but returns zero rows.

Default: LCDL enables CDP focus emulation on attach when OS focus steal is off — the tab thinks it is active without Page.bringToFront. See EDGE-CDP-ATTACH.md and OUTLOOK-READ-STRATEGIES.md.

List row fields (DOM harvest)

harvest_list_rows / list_rows evaluate scripts emit one object per visible OWA row:

Wire field DOM source (priority) Notes
from_email span[title] matching email e.g. vstrut@softserveinc.com
from email span text or avatar aria-label Display name; skip 1-char avatar initials
subject Longest substantive innerText line Full subject line — not sender name alone
received_raw span[title] full datetime → <time datetime> → short time label Transient; normalized in Python
received_at parse_list_received_at_to_utc UTC ISO …Z; browser TZ for time-only
preview Remaining body lines Optional; no trailing time duplicate
thread_id / item_id data-convid Stable OWA item id for Pass 2 re-find

Received: full hint (Sun 6/7/2026 9:27 AM) wins over short label (9:27 AM). Subject: store the complete line (e.g. …shared the folder "Projects" with you), not the sender display name.

Message identity (Exchange item id)

Primary: exchange_item_id (AAkALg…) from DOM harvest (data-itemid, href, parent walk in outlook_readonly.py harvest_list_rows). Wire: UnifiedItem.extra.exchange_item_id and list_row.exchange_item_id.

Cockpit derives message_identity_key = owa_item_message_key(exchange_item_id) for queue/dedupe. See outlook-message-identity.md.

DOM-only capture hits ~35% of rows at list time; menu intercept + scripts/backfill-outlook-exchange-item-ids.py cover the rest without RFC822/EML ingest.

Legacy content ui_hash (deprecated)

When Exchange id is unavailable on legacy rows only, consumers may still read compute_message_ui_hash (outlook_ui_hash.py):

ui_hash = SHA256( normalize(from_email or from) | normalize(subject) | received_at_utc_minute )

New LCDL harvest does not emit extra.ui_hash. Do not use content-hash for new catalog or disk paths.

EML artifact: governed task outlook_download_message_eml writes consumer dest_path (.eml only; .msg fails). OWA exports to Edge Downloads (subject filename); LCDL adopt_download_file moves into staging; Cockpit finalizes Message-ID basename. Dual matrix: OUTLOOK-EML-EXPORT-DUAL-ARCHITECTURE.md. When graph_id is supplied (from REST list), Cockpit/LCDL uses Graph GET /me/messages/{id}/$value first (outlook_download_message_mime_graph). Without graph_id, OWA export uses list-row context menu (right-click → Download) by default; reading-pane toolbar is fallback (FORGE_LCDL_OUTLOOK_EML_DOWNLOAD_MODE). Row resolve: FORGE_LCDL_OUTLOOK_RESOLVE_ROW_FUZZY=0 default (exact conv / row index). See OUTLOOK-TWO-PASS-PLAN.md.

Consumer integration

forge-cockpit-web must not implement REST URLs or list evaluate scripts in connectors/outlook.py. Set COCKPIT_OUTLOOK_LIST_STRATEGY=auto for guided harvest; map extra.is_unreadunified_message.is_read in lcdl_outlook_bridge.wire_item_to_cockpit_fields.

For download-then-analyze ingest, set COCKPIT_OUTLOOK_INGEST_MODE=download_then_analyze.

References