Handbook
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):
- Scroll list row into view (Cockpit + LCDL).
- HTTP
DownloadMessagewith cached x-token (FORGE_LCDL_OUTLOOK_OWA_SVC_HTTP_TIMEOUT_MS, default 20s). - On timeout or 401/400/403: invalidate token → context menu bootstrap → one HTTP retry.
- On further failure: adopt bootstrap
.emlfrom Downloads or full context-menu export (FORGE_LCDL_OUTLOOK_OWA_SVC_DISK_FALLBACK=1/ ui_fresh). - Outer
run_x_cdp_lane_loopmay invoke pure context-menu lane immediately after x-token lane returns Err.
Context-menu path (fallback / legacy mode):
- Snapshot Downloads (
snapshot_download_files) before menu click. - Resolve row — exact conv id /
owa:row:N; optional fuzzy subject/from only ifFORGE_LCDL_OUTLOOK_RESOLVE_ROW_FUZZY=1. - Context menu — Download as EML (no reading pane when
FORGE_LCDL_OUTLOOK_ALLOW_LIST_ROW_LEFT_CLICK=0). - Poll
wait_for_stable_eml_in_downloadsuntil size stable (or Playwrightdownloadevent fallback). adopt_download_file—shutil.moveto consumerdest_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 (v001–vNNN) |
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.
Related
- OUTLOOK-TWO-PASS-PLAN.md — discover vs download (legacy two-pass)
- OUTLOOK-LIST-STRATEGIES.md — Exchange item id identity
- outlook-message-identity.md — consumer contract
- MESSAGING-CONSUMER-BOUNDARY.md
- forge-cockpit-web outlook-owa-eml-dual-architecture.md