Outlook EML export — dual architecture (LCDL)

Consumer mirror: forge-cockpit-web outlook-owa-eml-dual-architecture.md. Identity: outlook-message-identity.md.

Updated

LCDL owns Playwright export mechanics and Downloads polling; Cockpit owns queue, paths, Message-ID filenames, adopt index, sync UI, and data layout versioning. Edge owns operator session and subject-based download names.

Cockpit applies layout upgrades (cockpit_data_manifest.json, chained upgrade/steps/vNNN_*.py, SQL schema, legacy source.eml rename) on startup before sync — LCDL is not involved. See consumer doc Data layout versioning.

Consumer tier (Cockpit-only): routine Outlook sync uses search-day complete_backward tier (Sync week / month / CLI) — one OWA received:DAY search per calendar day, with this sequential export engine per row. LCDL does not implement tier skip or day_fully_saved; see outlook-search-day-ingest.md.

Responsibility matrix

Concern forge-lcdl Cockpit consumer Edge
Governed export task outlook_download_message_eml Calls via lcdl_outlook_bridge / download_message_eml_via_lcdl OWA UI
Trigger export Default: outlook_download_owa_svc.py (HTTP DownloadMessage + cached x-token); fallback context menu (outlook_download_eml.py) Sets timeout, progress heartbeat Bootstrap menu once; direct HTTP per message
Wait for file Direct path: none (bytes to dest_path); menu path: wait_for_stable_eml_in_downloads Menu fallback writes ~/Downloads/{subject}.eml
Move to consumer path adopt_download_file(src, dest_tmp) dest_tmp = message dir _download.eml File leaves Downloads → “Removed” in flyout
Final .eml name finalize_eml_in_message_dir{Message-ID}.eml
Row locate before menu _RESOLVE_LIST_ROW_INDEX_JS in outlook_readonly.py Supplies list_row_id, conv_id, exchange_item_id, row_index, … List must show conv
x-token bootstrap bootstrap_owa_svc_context, ensure_owa_svc_token Forwards conv_id when list_row_id is owa:row:N Peek uses conv or AAkALg deep-link
Fail if row wrong list_row_conv_not_visible Maps to ingest failed / export failed counter
Tab visibility (background ingest) apply_cdp_focus_emulation on CDP attach (Emulation.setFocusEmulationEnabled) Relies on LCDL defaults (FORGE_LCDL_CDP_BRING_TO_FRONT=0) Operator window may stay in background
Orphan Downloads cleanup cleanup_owa_eml_left_in_downloads Invokes after adopt_eml_from_downloads when unmatched=0
Graph MIME (optional) outlook_download_message_mime_graph Skipped when COCKPIT_OUTLOOK_OWA_ONLY=1 Session cookies
Data layout / layout_version cockpit_server/upgrade/, manifest, splash
Legacy source.eml rename Startup step v004 + eml_paths.migrate_legacy_source_eml Subject filenames in Downloads only

Export sequence (LCDL)

owa_svc_direct (default with ui_fresh):

  1. Scroll list row into view (Cockpit + LCDL).
  2. HTTP DownloadMessage with cached x-token (FORGE_LCDL_OUTLOOK_OWA_SVC_HTTP_TIMEOUT_MS, default 20s).
  3. On timeout or 401/400/403: invalidate token → context menu bootstrap → one HTTP retry.
  4. On further failure: adopt bootstrap .eml from Downloads or full context-menu export (FORGE_LCDL_OUTLOOK_OWA_SVC_DISK_FALLBACK=1 / ui_fresh).
  5. Outer run_x_cdp_lane_loop may invoke pure context-menu lane immediately after x-token lane returns Err.

Context-menu path (fallback / legacy mode):

  1. Snapshot Downloads (snapshot_download_files) before menu click.
  2. Resolve row — exact conv id / owa:row:N; optional fuzzy subject/from only if FORGE_LCDL_OUTLOOK_RESOLVE_ROW_FUZZY=1.
  3. Context menu — Download as EML (no reading pane when FORGE_LCDL_OUTLOOK_ALLOW_LIST_ROW_LEFT_CLICK=0).
  4. Poll wait_for_stable_eml_in_downloads until size stable (or Playwright download event fallback).
  5. adopt_download_fileshutil.move to consumer dest_path, then _remove_src_if_still_present.

Cockpit then verifies headers + message identity on staging bytes and finalizes under files/…/messages/…/. See outlook-message-identity.md.

Downloads directory

Function Module Role
edge_downloads_directory() outlook_download_disk.py ~/Downloads or FORGE_LCDL_OUTLOOK_EDGE_DOWNLOADS_DIR
adopt_download_file same Move complete .eml to consumer staging
cleanup_owa_eml_left_in_downloads same Delete aged .eml leftovers (consumer gates on unmatched adopt)

LCDL must not rename to Message-ID, write download.json, or touch Cockpit files/ layout beyond the path passed in dest_path.

Row resolution (LCDL contract)

Input Resolution
list_row_id with data-convid Match visible row with same conv attribute
owa:row:N Exact list index in viewport harvest
Subject/from hints Used only when FORGE_LCDL_OUTLOOK_RESOLVE_ROW_FUZZY=1

Errors surfaced to consumer:

PlaywrightError.message Typical cause
list_row_conv_not_visible Conv scrolled off screen; stale index
eml_download_timeout Edge slow or menu blocked
owa_svc_http_timeout HTTP DownloadMessage exceeded cap; triggers token refresh then menu fallback

Environment (LCDL-facing)

Variable Default when Cockpit sets OWA-only Effect
FORGE_LCDL_OUTLOOK_EML_DOWNLOAD_MODE owa_svc_direct Direct HTTP vs context menu vs reading pane
FORGE_LCDL_OUTLOOK_OWA_SVC_DISK_FALLBACK 0 Context-menu disk export after HTTP + token refresh fail (Cockpit ui_fresh sets 1)
FORGE_LCDL_OUTLOOK_OWA_SVC_HTTP_TIMEOUT_MS 20000 Per HTTP GET cap before menu/disk fallback
FORGE_LCDL_OUTLOOK_EML_ALLOW_READING_PANE_FALLBACK 0 Avoid left-click row
FORGE_LCDL_OUTLOOK_RESOLVE_ROW_FUZZY 0 Strict row match
FORGE_LCDL_OUTLOOK_ALLOW_LIST_ROW_LEFT_CLICK 0 Discover scroll without preview
FORGE_LCDL_OUTLOOK_DOWNLOAD_POLL_SEC 0.25 Downloads poll interval
FORGE_LCDL_OUTLOOK_EDGE_DOWNLOADS_DIR (unset) Override Downloads path

Cockpit copies COCKPIT_OUTLOOK_EML_* into FORGE_LCDL_OUTLOOK_* in apply_outlook_eml_download_env_defaults() (outlook_sync_tuning.py).

What LCDL must not do

Forbidden in LCDL Belongs in Cockpit
Message-ID file naming eml_paths.py
message_ingest_queue updates outlook_download_pass.py
Adopt index / quarantine eml_adopt_downloads.py
Sync progress stat grid SyncProgressPanel.tsx
Message identity local presence outlook_local_presence / bridge (exchange_item_id, message_identity_key)
Read/write cockpit_data_manifest.json or layout_version upgrade/runner.py, manifest on disk
Chained layout upgrade steps (v001vNNN) cockpit_server/upgrade/steps/ — runs before HTTP/sync
SQL schema migrations (schema/*.sql) Step v003 + _schema_migrations
Bulk rename source.eml{message-id}.eml under files/ Step v004 / migrate_legacy_source_eml
Startup splash / COCKPIT_STARTUP_EVENTS progress Electron desktop/main.js

Versioning rule: If export or Downloads behavior changes in LCDL, update both dual-architecture docs. If on-disk layout or SQLite shape must change for consumers, add a new Cockpit upgrade step only — do not embed migration loops in LCDL tasks or Playwright helpers.