Files
WhispAssist/docs/adr/0008-pst-calendar.md
T

6.2 KiB

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 PropertyValues. 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).