96 lines
6.2 KiB
Markdown
96 lines
6.2 KiB
Markdown
# ADR-0008 — Calendar & Outlook .pst integration
|
|
|
|
- **Status:** Accepted
|
|
- **Date:** 2026-06-30
|
|
- **Context source:** Design doc §"Calendar Integration", §"Outlook .pst Integration"
|
|
|
|
## Context
|
|
|
|
WA enriches meetings with calendar context (title, organizer, attendees) and reads local Outlook
|
|
`.pst` backups to build meeting history and suggest speaker names — all local, no cloud sync
|
|
required.
|
|
|
|
## Decision
|
|
|
|
- **`.pst` reading:** use the Rust **`outlook-pst`** crate (read-only, MS-PST-based) to extract
|
|
calendar appointments and attendees. Fall back to **libpff** (`pffexport`/FFI or a small
|
|
sidecar) for password-protected or edge-case files. PST is **read-only input**; WA never writes
|
|
to it.
|
|
- **Live calendar:** abstract behind a `calendar::CalendarSource` trait. Phase 6 ships the PST
|
|
source. Microsoft Graph (with explicit user consent) and local ICS files are future sources
|
|
behind the same trait; Graph is the _one_ allowed remote call and only for calendar metadata,
|
|
never meeting content, and only with consent.
|
|
- **Reminders:** action items become **local OS notifications** (Windows toast); optional
|
|
calendar entry creation is deferred and, where added, stays local/consented.
|
|
- **Participant-aware naming:** attendee names from the selected event populate the speaker-naming
|
|
dropdown (FR-SPK-2), with an "add new name" escape hatch; mappings persist per meeting.
|
|
|
|
## Consequences
|
|
|
|
- **Positive:** delivers Granola-style context fully offline from data the user already has;
|
|
trait keeps Graph/ICS additions non-invasive; read-only PST avoids corrupting user mail stores.
|
|
- **Negative:** PST parsing is fiddly (encryption, large files, format variance) → keep libpff as
|
|
a fallback and treat parse failures as non-fatal; Graph path (future) introduces OAuth + the
|
|
product's only remote dependency, so it must be strictly opt-in and clearly labeled.
|
|
|
|
## Update (2026-07-01) — `readpst` (libpst) instead of the `outlook-pst` crate
|
|
|
|
T6.1 implementation revealed `outlook-pst` (the Rust crate this ADR originally named) is a
|
|
byte-level, "clean room" implementation of the raw MS-PST binary format — B-tree pages,
|
|
allocation maps, block trailers — with **no MAPI object-model layer**: no `folder.name()` or
|
|
`message.subject()`, just numeric property IDs and hand-decoded `PropertyValue`s. Reaching
|
|
"appointment with attendees" would mean hand-implementing named-property resolution
|
|
(`PidLidAppointmentStartWhole` etc. aren't fixed low prop IDs) and per-message recipient-table
|
|
parsing against the MS-PST/MS-OXPROPS spec directly, with no test `.pst` fixture available to
|
|
verify correctness against.
|
|
|
|
Decision: shell out to **`readpst`** (the **libpst** project, not libpff — the two are separate,
|
|
similarly-purposed C libraries) instead. `readpst -S -e -t a -o <dir> <file.pst>` dumps every
|
|
appointment as its own `.ics` (iCalendar) file; WA parses those with a small hand-rolled RFC 5545
|
|
VEVENT reader (`calendar::parse_vevents` — SUMMARY/ORGANIZER/DTSTART/DTEND/ATTENDEE/UID only, no
|
|
timezone database, floating and `Z` times both treated as UTC). This trades a pure-Rust dependency
|
|
for an external-binary one (`readpst` must be on `PATH` — obtainable via MSYS2, Cygwin, or
|
|
prebuilt Windows binaries; WA surfaces a clear `CalError::ToolMissing` if it isn't found, no
|
|
different in kind from Ollama's guided-install treatment, T5.7) in exchange for a **far** smaller,
|
|
better-tested WA-side surface. Most PST "password protection" is a UI-level gate over a fixed,
|
|
keyless obfuscation that `readpst` already reverses transparently, not real encryption — the
|
|
`password` field is accepted for forward-compat but currently unused; genuinely IRM-protected
|
|
files surface as a non-fatal parse error rather than being supported.
|
|
|
|
`CalendarSource::attendees(event_id)` (a second, separate trait method in the original design) was
|
|
dropped: PST/ICS list attendees inline per-appointment, so there's no live "fetch attendees for
|
|
event X" round trip against the source to make — `import()` now returns `Vec<ImportedEvent>`
|
|
(event + its attendees together), and storage persists both.
|
|
|
|
## Update (2026-07-01) — validated against a real 7.2GB mailbox
|
|
|
|
Ran `readpst` (libpst v0.6.63, a ~2014 Windows build — an older release than current libpst
|
|
master) against a real, in-use corporate `.pst` and fed every resulting `.ics` file through the
|
|
real `parse_vevents`. Results: **1738/1738 files produced exactly one valid event each** —
|
|
subjects, `DTSTART`/`DTEND` (including the `VALUE=DATE-TIME` parameter form, correctly ignored by
|
|
the parser) all came through cleanly, zero parse failures.
|
|
|
|
One real, confirmed gap: **this libpst v0.6.63 build never emits `ORGANIZER` or `ATTENDEE` lines
|
|
at all** — 0/1738 events had either, including confirmed real meetings ("Denver", location
|
|
"Microsoft Teams Meeting", `STATUS:TENTATIVE`). `write_schedule_part_data()`'s attendee-writing
|
|
logic either doesn't exist in this old release or isn't reached; not something WA's parser can
|
|
compensate for — the data simply never reaches WA. Practical effect: **T6.1/T6.2 deliver real
|
|
event import (subject/organizer-as-None/start/end/location) today; T6.5's attendee-populated
|
|
naming dropdown has nothing to populate from PST until this is resolved.** Also found and fixed:
|
|
this readpst build doesn't support `-8` (force UTF-8 output) at all ("invalid option -- 8") —
|
|
removed from the invocation; a non-UTF-8-encoded item's `.ics` file is skipped rather than
|
|
misread (`collect_ics_files` already treats a non-UTF-8-readable file as skippable).
|
|
|
|
Also confirmed empirically: `-t a` does mean **appointments**, not attachments, despite this
|
|
build's own `-h` output literally printing `-t[eajc] ... a = attachment` — the printed usage
|
|
string is wrong/stale in this build; the real behavior (verified by watching it skip-count
|
|
non-appointment items per folder, then produce exactly the Calendar folder's real appointments)
|
|
matches current libpst master's `OTMODE_APPOINTMENT` mapping for `'a'`.
|
|
|
|
## Revisit if
|
|
|
|
A newer libpst build (or a different install path) turns out to emit `ORGANIZER`/`ATTENDEE`
|
|
correctly — re-test T6.5 once one is available. Otherwise: `readpst` proves insufficient in
|
|
practice in some other way, or users need live Exchange more than PST (prioritize the Graph
|
|
source).
|