This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OutlookAddin/docs/PROJECT-BRIEFING.md
PointStar a8e947c825 docs: reflect v1.2.9 auto-update settings + v1.2.7 button behavior
Updates after the day-long updater overhaul:

- The csproj no longer has <UpdateInterval>7</UpdateInterval>; v1.2.9
  switched to <UpdatePeriodically>false</UpdatePeriodically>, so the
  VSTO runtime checks the .vsto manifest on every Outlook startup.
  Both PROJECT-BRIEFING.md ("How releases work") and
  AUTO-UPDATE-SETUP.md (section 5) now describe the on-startup model
  instead of the weekly-poll model that was never operationally true.
- AUTO-UPDATE-SETUP.md also gets two new notes that came out of today:
  what the "בדוק עדכונים" button can and can't do (informational only,
  pointing at the in-process-update impossibility memo), and the
  one-time uninstall+reinstall required when a developer's machine has
  a dev-cert install that can't auto-update across to the prod cert.

No code change; no tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:14:00 +03:00

18 KiB

Project Briefing — OutlookAddin

Purpose of this document: self-contained briefing for a fresh Claude Code session (or any new developer) picking up work on this project. Read this before doing anything.


TL;DR

A Windows VSTO/COM Outlook Add-in (C# / .NET Framework 4.8) that lets 10 lawyers at marcus-law.co.il file emails from Outlook desktop into EspoCRM. The server-side (MailRouter v1.2.0) is already deployed and ready. This repo is the client.

Target Outlook versions: Microsoft 365, Office 2021, Office 2019 (not 2016). The team uses IMAP-connected Gmail mailboxes — this is why Office.js Web Add-ins won't work and we went VSTO.


The four features the Add-in must deliver

  1. File existing email to case/contact — Ribbon button in Explorer; opens a WPF dialog with live autocomplete; POSTs the selected mail(s) with attachments to POST /api/v1/MailRouter/file; categorizes filed mail with a colored Outlook category.

  2. Reading-pane sidebarCustomTaskPane docked right (320px). On selection change, looks up the sender via GET /api/v1/EmailAddress/search?q=&entityType=Contact and shows matched Case/Contact cards with "Open in EspoCRM", "File to this", "File elsewhere".

  3. Compose from Case (bidirectional)

    • Outlook → EspoCRM: Inspector ribbon button "Compose from Case" → picker → fills To/Subject/footer from GET /api/v1/Case/<id>; stamps MailItem.UserProperties["EspoCaseId"].
    • EspoCRM → Outlook: URL protocol outlookaddin://compose?caseId=<id> registered at install (HKCU); a tiny handler exe pipes the request to the running Add-in.
    • ItemSend handler: when EspoCaseId is set, auto-file the sent message to that case (with 60s fallback for IMAP latency in Sent Items sync).
  4. Auto-sync folders — Settings tab "Folders" with TreeView + checkboxes. FolderWatcher subscribes to Items.ItemAdd on watched folders (must hold Items references in a static field — VSTO footgun!). New mail → MatchingService → confident match → auto-file, else categorize "Needs filing" + toast.


Server-side contract (already deployed — DO NOT MODIFY)

POST /api/v1/MailRouter/file

Authoritative source: MailRouter/files/.../FileEmailService.php:9-26.

{
  "messageId": "<rfc2822-id@server>",
  "subject":  "string",
  "from":     "Name <addr>",
  "to":       ["addr1", "addr2"],
  "cc":       ["..."],
  "bcc":      ["..."],
  "replyTo":  ["..."],
  "body":     "string (plain or HTML)",
  "isHtml":   true,
  "dateSent": "2026-05-11T08:30:00Z",
  "parentType": "Case",
  "parentId":   "<entity-id>",
  "attachments": [
    { "name": "doc.pdf", "contentType": "application/pdf", "contentBase64": "..." }
  ]
}

Response:

{ "id": "<emailId>", "created": true, "parentType": "Case", "parentId": "<id>", "messageId": "<msgId>" }
  • Dedupes by messageId (set parent on existing record, returns created: false).
  • parentType{ Case, Lead, Contact, Account }.
  • 401 Forbidden if not authenticated.
  • 400 BadRequest if parentType/parentId missing.

Other endpoints the Add-in uses

Endpoint Purpose
GET /api/v1/GlobalSearch?q=<text> Free-text autocomplete across Case/Contact/Lead/Account
GET /api/v1/EmailAddress/search?q=<addr>&entityType=Contact Find contact by email address
GET /api/v1/Case?q=&maxSize=20 Entity-specific autocomplete
GET /api/v1/Case/<id>?select=name,number,contactsIds,accountId Fetch case for compose flow
GET /api/v1/Account/<id> Follow account from contact
GET /api/v1/Case?where[0][type]=linkedWith&where[0][attribute]=contacts&where[0][value]=<contactId>&maxSize=5 Recent cases for contact
GET /api/v1/App/user Test connection (returns current user)

Authentication

Espo-Authorization: <base64(username:apiKey)>

Each lawyer gets an API key issued by the admin via EspoCRM Admin → API Users (or per-user via User Profile). Stored locally encrypted with DPAPI.

Environments

Env Base URL
dev https://espocrm.dev.marcus-law.co.il
prod https://crm.prod.marcus-law.co.il

Tech stack (locked, do not relitigate)

Concern Pick
Runtime .NET Framework 4.8 (VSTO Runtime targets desktop CLR, .NET 6+ VSTO is rough)
UI WPF (in ElementHost inside WinForms UserControl for CustomTaskPane)
HTTP HttpClient + Polly (Timeout 15s → Retry 3 exp → CircuitBreaker 5/30s)
JSON System.Text.Json (NuGet polyfill on 4.8)
Logging Serilog + Serilog.Sinks.File daily rolling, 14-day retention
MVVM CommunityToolkit.Mvvm (source generators)
Tests xUnit + Moq + FluentAssertions
DI Manual composition root in ThisAddIn_Startup (no container)
Settings JSON in %APPDATA%\MarcusLaw\OutlookAddin\settings.json
Secrets DPAPI in %APPDATA%\MarcusLaw\OutlookAddin\creds.dat, entropy = sha256("MarcusLaw.OutlookAddin.v1")
Logs %LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs\YYYY-MM-DD.log

Namespace root: MarcusLaw.OutlookAddin.*


Repo location

Gitea https://gitea.dev.marcus-law.co.il/espocrm-extensions/OutlookAddin
Clone URL (HTTPS w/ token) https://chaim:19771681b7dc9939e4e597f7dae3a2bad87f4084@gitea.dev.marcus-law.co.il/espocrm-extensions/OutlookAddin.git
Local (Windows) <user-chosen>\OutlookAddin\ (currently C:\Users\<user>\source\repos\OutlookAddin)
Local (Linux dev server) /home/chaim/espocrm-extensions/OutlookAddin/

The local-dev paths in this doc reference the Linux server (/home/chaim/...) because the initial scaffold was created there. After cloning to Windows, paths become C:\Users\<user>\source\repos\OutlookAddin\....


Solution structure (planned)

OutlookAddin.sln
├── src\
│   ├── OutlookAddin\                  ← VSTO host (.NET FW 4.8, legacy csproj from VS template)
│   │   ├── ThisAddIn.cs               composition root, ItemSend/ItemAdd wiring
│   │   ├── Ribbon\{Main,Compose}Ribbon.{xml,cs}
│   │   ├── TaskPanes\ReadingPane{Control.cs,View.xaml}
│   │   ├── EventHandlers\{ItemSendHandler,FolderWatcher,ExplorerSelectionHandler}.cs
│   │   └── UrlProtocol\OutlookAddinProtocolHandler.cs
│   ├── OutlookAddin.Core\             ← pure .NET (Office-free, testable on Linux)
│   │   ├── Models\                    DTOs
│   │   ├── Services\{EspoCrmClient,FilingService,MatchingService,DiskRetryQueue}.cs
│   │   ├── Security\DpapiCredentialStore.cs
│   │   ├── Configuration\{AppSettings,SettingsManager}.cs
│   │   └── Logging\LoggerFactory.cs
│   ├── OutlookAddin.UI\               ← WPF dialogs + ViewModels
│   │   ├── Dialogs\{FileToCase,Settings,ComposeFromCase}Dialog.xaml
│   │   ├── ViewModels\
│   │   ├── Controls\{AutocompleteTextBox,RecentEntitiesList}.cs
│   │   ├── Localization\{Strings.he.resx,Strings.en.resx,LocalizationManager.cs}
│   │   └── Themes\{Generic,RtlFonts}.xaml
│   └── OutlookAddin.Tests\            ← xUnit
├── installer\OutlookAddin.Setup\       ← WiX / ClickOnce manifest (week 6)
├── docs\
│   ├── ARCHITECTURE.md                ← component diagram + threading + per-feature flow
│   ├── ONBOARDING.md                  ← Hebrew lawyer-facing install guide
│   └── PROJECT-BRIEFING.md            ← this file
└── .gitea\workflows\build.yml         ← Windows runner CI (deferred — local builds for now)

Current state of the repo

Done (week 0 — infrastructure)

  1. Gitea repo created (private, espocrm-extensions/OutlookAddin)
  2. Directory tree scaffolded
  3. OutlookAddin.sln + 3 SDK-style csproj (Core, UI, Tests) — all load cleanly in VS 2022
  4. OutlookAddin.csproj (host) — hand-written legacy csproj is INCOMPATIBLE; VS marks it "Unsupported". MUST be regenerated from the VS template.
  5. .gitignore, README.md, docs/ARCHITECTURE.md, docs/ONBOARDING.md
  6. .gitea/workflows/build.yml stub (won't run until Windows runner exists)
  7. Infisical folders /outlook-addin created in dev/prod/nautilus (empty — secrets added when needed)
  8. Task Master initialized at .taskmaster/ with 9 tasks matching the 6-week plan, using provider=claude-code (free)
  9. Initial 2 commits pushed to Gitea main

🚧 In progress — first thing to do on Windows

Recreate the VSTO host project from the official template. Step-by-step:

  1. Open OutlookAddin.sln in VS 2022.
  2. When you see "Review Project And Solution Changes" → "Unsupported" → click OK (VS loads the other 3 projects).
  3. Solution Explorer → right-click OutlookAddin (unloaded/missing) → Remove (not Delete — Remove just unlists it).
  4. Delete the stale source files the Linux scaffold created. From a Git Bash / PowerShell at the repo root:
    rm -rf src/OutlookAddin
    
    (or in Windows Explorer: delete the src\OutlookAddin\ folder entirely.)
  5. FileAddNew Project... → search "outlook" → select Outlook VSTO Add-in (C#) → Next.
  6. Configure:
    • Project name: OutlookAddin (exact — not MarcusLaw.OutlookAddin)
    • Location: <repo>\src\ (the parent src folder)
    • Solution: Add to solution
    • Framework: .NET Framework 4.8
    • Click Create.
  7. VS generates src\OutlookAddin\OutlookAddin.csproj + ThisAddIn.cs + ThisAddIn.Designer.cs + app.config + Properties\AssemblyInfo.cs.
  8. Set the assembly metadata — open Properties\AssemblyInfo.cs and set:
    • [assembly: AssemblyCompany("Marcus-Law")]
    • [assembly: AssemblyProduct("OutlookAddin")]
    • Root namespace: right-click project → Properties → Application → Default namespace = MarcusLaw.OutlookAddin, Assembly name = MarcusLaw.OutlookAddin
  9. Right-click OutlookAddin → Add → Project Reference... → check OutlookAddin.Core and OutlookAddin.UI → OK.
  10. Build Solution (Ctrl+Shift+B) — expect success.
  11. F5 — VS should launch Outlook with the Add-in loaded (no UI yet, but it shouldn't crash). Close Outlook to return to VS.
  12. Commit + push the regenerated host project:
    git add src/OutlookAddin
    git commit -m "chore: regenerate OutlookAddin host project from VSTO template"
    git push
    

📋 After that — start Task #2 (Core services)

Implement OutlookAddin.Core.Services.EspoCrmClient per the design in ARCHITECTURE.md and per the API contracts in this doc. See Task Master task #2 for the full breakdown:

task-master show 2

Plan file (long-term)

Full 6-week implementation plan with risks, verification steps, and the deployment strategy:

/home/chaim/.claude/plans/resilient-sauteeing-feather.md (on the Linux dev server).

A copy in this repo is not committed because the plan is workspace-wide context. If a Claude session needs it, it can be fetched via SSH:

scp chaim@192.168.10.206:/home/chaim/.claude/plans/resilient-sauteeing-feather.md .

Important repo conventions

  1. Conventional commits: fix:, feat:, chore:, docs:, refactor:, test:.
  2. Co-Author footer: every Claude-assisted commit ends with
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
  3. No emojis in commits or code unless explicitly requested by the user.
  4. No backwards-compat shims — change code directly, no // removed comments, no renamed-with-underscore unused vars.
  5. No comments explaining WHAT — only WHY when the why is non-obvious (hidden constraints, workarounds for bugs, surprising behavior).
  6. Default to no comments at all.
  7. Branching: main is protected; feature branches feat/<short-name> or fix/<short-name>.
  8. Task Master: every code change should correspond to a Task Master task. See task-master list to find the next task.

Secrets / infrastructure references

What Where
EspoCRM API keys (per-lawyer) Issued by EspoCRM admin; stored locally in Add-in's DPAPI store
Code-signing cert (TBD — week 6) Infisical /outlook-addin/CODESIGN_THUMBPRINT, CODESIGN_PFX_BASE64, CODESIGN_PASSWORD
ClickOnce deploy SSH key (TBD) Infisical /outlook-addin/PLATFORM_DEV_SSH_KEY
Gitea HTTPS token 19771681b7dc9939e4e597f7dae3a2bad87f4084 (chaim user, in this doc URL above)
MailRouter source (read-only ref) https://gitea.dev.marcus-law.co.il/espocrm-extensions/MailRouter
Infisical Project ID 9a77b161-f70c-4dd3-9d67-b7ab850cef51
Infisical envs dev (Windows runner / dev server), prod (Hetzner), nautilus

How to brief a new Claude session

Tell the new session:

Read docs/PROJECT-BRIEFING.md in this repo. Then read docs/ARCHITECTURE.md. Then run task-master list to see open tasks. Don't start coding until you've read both docs and confirmed which task you're working on with me.

If the session is on the Linux dev server (/home/chaim/espocrm-extensions/OutlookAddin), also have it check the long-term plan at ~/.claude/plans/resilient-sauteeing-feather.md.


How releases work (current, 2026-05-24)

Production install URL: https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto

Release flow:

  1. Code change merged to main (push or PR-merge — no CI fires yet)
  2. Bump <ApplicationVersion> in src/OutlookAddin/OutlookAddin.csproj and AssemblyVersion + AssemblyFileVersion in src/OutlookAddin/Properties/AssemblyInfo.cs to the next 4-part version (e.g. 1.2.1.0). CI overrides ApplicationVersion from the tag at build time, but leaves AssemblyInfo alone — keep them in sync manually so the bundled DLL's FileVersion matches the manifest.
  3. git tag -a v1.2.1 -m "..." && git push --tags
  4. Gitea Actions workflow (.gitea/workflows/build.yml) fires:
    • Imports the real codesign cert from secrets CODESIGN_CERT_PFX_BASE64 + CODESIGN_THUMBPRINT (publicKeyToken c68d2b4c25051c5b, CN=Marcus-Law OutlookAddin)
    • Patches csproj to swap the dev self-signed thumbprint (2399E9BF…) for the real one
    • msbuild /t:Publish with /p:InstallUrl=https://platform.dev.marcus-law.co.il/outlook-addin/ and /p:ApplicationVersion=<tag>.0
    • Uploads publish.tar.gz to Gitea generic registry (outlook-addin-publish/<version>/publish.tar.gz)
    • Linux job downloads the tar.gz, builds a Docker image outlook-addin-cdn:latest, pushes to Gitea registry, calls Coolify to redeploy the CDN container
    • Posts to Mattermost channel git-verelasim with the install URL on success / a failure link on failure
  5. Lawyers' installed Add-in checks the deployment manifest at the install URL on every Outlook startup (csproj has <UpdatePeriodically>false</UpdatePeriodically> since v1.2.9 — earlier versions waited up to 7 days). The check is a single ~6 KB HTTP GET, ~100 ms. Combined with the CI setting /p:MinimumRequiredVersion=<tag> per build, this means every tagged release lands on every lawyer's PC the next time they open Outlook, silently and without prompts.

Semver policy: PATCH for bug fixes (unilateral), MINOR for visible features (unilateral when warranted), MAJOR only with Chaim's explicit approval.

Branding (KlearBranding repo): the product is presented to lawyers as Klear — title bars, MessageBox titles, ribbon group label, About header all say "Klear". The logo is mirrored from espocrm-extensions/KlearBranding/files/client/custom/img/logo-klear.png into src/OutlookAddin.UI/Resources/logo-klear.png and embedded as a WPF Resource. Brand accent color: #121d2e (dark navy).

What stays MarcusLaw under the hood (deliberate — preserves auto-update across the rebrand):

  • ClickOnce app identity (AssemblyName=MarcusLaw.OutlookAddin)
  • %APPDATA%\MarcusLaw\OutlookAddin\settings.json and creds.dat
  • %LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs\
  • DpapiCredentialStore.EntropySeed = "MarcusLaw.OutlookAddin.v1"
  • Code namespaces MarcusLaw.OutlookAddin.*
  • Named pipe MarcusLaw.OutlookAddin

Changing any of these would orphan all existing installs and require a coordinated uninstall+reinstall on every lawyer's PC plus an entropy migration for DPAPI-encrypted credentials. See the LegalCRM rebrand misadventure in git history for what not to do.

Known traps when releasing:

  • VS Publish wizard (Project Properties → Publish) rewrites csproj at every click — flips <AutoIncrementApplicationRevision> back to true, resets <ApplicationVersion> to whatever's in the UI, sometimes deletes prior Application Files/<...>/ bundles. Don't use it. Tag and let CI do it.
  • There's an old Publish/ directory in git history that was a parallel manual-release channel via gitea.../raw/branch/main/Publish/. It's not the production install URL — that's platform.dev. The folder was dropped in v1.2.0.

Things deliberately deferred

What When Why deferred
Identity-level rebrand to a pure Klear product (AssemblyName, namespaces, DPAPI entropy, AppData paths) Until there's another reason to force-reinstall on all 10 PCs Visible rebrand already done in v1.2.0 without disrupting users; full rename trades aesthetic cleanliness for a day of IT coordination + lost credentials

Open risks (from the plan)

  • VSTO Items event handler GC unsubscribe (folder watcher footgun) — must hold Items refs in static field.
  • IMAP PR_INTERNET_MESSAGE_ID race on Sent Items — use UserProperty GUID + 60s watcher fallback.
  • Outlook HTML quirks (VML, conditional comments) — send both plain Body and HTMLBody, let server decide.
  • API key rotation → silent 401 — show non-dismissable modal, log key fingerprint hash.
  • Base64-of-50MB-attachment memory blowup — sequential processing, 25MB single-attachment cap.
  • Hebrew RTL in subject prefix [2025-123] — use Unicode LRM/RLM bidi marks.

See full risk table in ARCHITECTURE.md and the plan file.