Replaces the stale "Things deliberately deferred" entries (CI runner, codesign cert, platform.dev hosting, Mattermost webhook) with a new "How releases work (current)" section that captures what's actually running today: - Production install URL = platform.dev.marcus-law.co.il/outlook-addin/ - Tag-driven Gitea Actions pipeline in .gitea/workflows/build.yml - Real codesign cert lives in Gitea secrets, dev cert in repo - Klear visible rebrand boundary: what shipped in v1.2.0 vs what stays MarcusLaw to preserve auto-update across the 10 installed lawyer PCs - The dropped Publish/ folder was never the production channel The "deferred" table now only mentions the identity-level Layer-2 rebrand, which is the only thing actually still open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
-
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. -
Reading-pane sidebar —
CustomTaskPanedocked right (320px). On selection change, looks up the sender viaGET /api/v1/EmailAddress/search?q=&entityType=Contactand shows matched Case/Contact cards with "Open in EspoCRM", "File to this", "File elsewhere". -
Compose from Case (bidirectional) —
- Outlook → EspoCRM: Inspector ribbon button "Compose from Case" → picker → fills To/Subject/footer from
GET /api/v1/Case/<id>; stampsMailItem.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).
- Outlook → EspoCRM: Inspector ribbon button "Compose from Case" → picker → fills To/Subject/footer from
-
Auto-sync folders — Settings tab "Folders" with TreeView + checkboxes.
FolderWatchersubscribes toItems.ItemAddon watched folders (must holdItemsreferences 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, returnscreated: false). parentType∈{ Case, Lead, Contact, Account }.- 401 Forbidden if not authenticated.
- 400 BadRequest if
parentType/parentIdmissing.
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)
- Gitea repo created (private,
espocrm-extensions/OutlookAddin) - Directory tree scaffolded
OutlookAddin.sln+ 3 SDK-style csproj (Core,UI,Tests) — all load cleanly in VS 2022OutlookAddin.csproj(host) — hand-written legacy csproj is INCOMPATIBLE; VS marks it "Unsupported". MUST be regenerated from the VS template..gitignore,README.md,docs/ARCHITECTURE.md,docs/ONBOARDING.md.gitea/workflows/build.ymlstub (won't run until Windows runner exists)- Infisical folders
/outlook-addincreated in dev/prod/nautilus (empty — secrets added when needed) - Task Master initialized at
.taskmaster/with 9 tasks matching the 6-week plan, usingprovider=claude-code(free) - 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:
- Open
OutlookAddin.slnin VS 2022. - When you see "Review Project And Solution Changes" → "Unsupported" → click OK (VS loads the other 3 projects).
- Solution Explorer → right-click
OutlookAddin(unloaded/missing) → Remove (not Delete — Remove just unlists it). - Delete the stale source files the Linux scaffold created. From a Git Bash / PowerShell at the repo root:
(or in Windows Explorer: delete the
rm -rf src/OutlookAddinsrc\OutlookAddin\folder entirely.) File→Add→New Project...→ search "outlook" → select Outlook VSTO Add-in (C#) → Next.- Configure:
- Project name:
OutlookAddin(exact — notMarcusLaw.OutlookAddin) - Location:
<repo>\src\(the parentsrcfolder) - Solution: Add to solution
- Framework: .NET Framework 4.8
- Click Create.
- Project name:
- VS generates
src\OutlookAddin\OutlookAddin.csproj+ThisAddIn.cs+ThisAddIn.Designer.cs+app.config+Properties\AssemblyInfo.cs. - Set the assembly metadata — open
Properties\AssemblyInfo.csand set:[assembly: AssemblyCompany("Marcus-Law")][assembly: AssemblyProduct("OutlookAddin")]- Root namespace: right-click project → Properties → Application → Default namespace =
MarcusLaw.OutlookAddin, Assembly name =MarcusLaw.OutlookAddin
- Right-click
OutlookAddin→ Add → Project Reference... → checkOutlookAddin.CoreandOutlookAddin.UI→ OK. - Build Solution (Ctrl+Shift+B) — expect success.
- F5 — VS should launch Outlook with the Add-in loaded (no UI yet, but it shouldn't crash). Close Outlook to return to VS.
- 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
- Conventional commits:
fix:,feat:,chore:,docs:,refactor:,test:. - Co-Author footer: every Claude-assisted commit ends with
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> - No emojis in commits or code unless explicitly requested by the user.
- No backwards-compat shims — change code directly, no
// removedcomments, no renamed-with-underscore unused vars. - No comments explaining WHAT — only WHY when the why is non-obvious (hidden constraints, workarounds for bugs, surprising behavior).
- Default to no comments at all.
- Branching:
mainis protected; feature branchesfeat/<short-name>orfix/<short-name>. - Task Master: every code change should correspond to a Task Master task. See
task-master listto 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.mdin this repo. Then readdocs/ARCHITECTURE.md. Then runtask-master listto 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:
- Code change merged to
main(push or PR-merge — no CI fires yet) - Bump
<ApplicationVersion>insrc/OutlookAddin/OutlookAddin.csprojandAssemblyVersion+AssemblyFileVersioninsrc/OutlookAddin/Properties/AssemblyInfo.csto the next 4-part version (e.g.1.2.1.0). CI overridesApplicationVersionfrom the tag at build time, but leaves AssemblyInfo alone — keep them in sync manually so the bundled DLL's FileVersion matches the manifest. git tag -a v1.2.1 -m "..." && git push --tags- Gitea Actions workflow (
.gitea/workflows/build.yml) fires:- Imports the real codesign cert from secrets
CODESIGN_CERT_PFX_BASE64+CODESIGN_THUMBPRINT(publicKeyTokenc68d2b4c25051c5b,CN=Marcus-Law OutlookAddin) - Patches csproj to swap the dev self-signed thumbprint (
2399E9BF…) for the real one msbuild /t:Publishwith/p:InstallUrl=https://platform.dev.marcus-law.co.il/outlook-addin/and/p:ApplicationVersion=<tag>.0- Uploads
publish.tar.gzto 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-verelasimwith the install URL on success / a failure link on failure
- Imports the real codesign cert from secrets
- Lawyers' installed Add-in checks the deployment manifest at the install URL every 7 days (configured in csproj
<UpdateInterval>7</UpdateInterval>) or whenever they click "בדוק עדכונים" in Settings → אודות.
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.jsonandcreds.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 totrue, resets<ApplicationVersion>to whatever's in the UI, sometimes deletes priorApplication 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 viagitea.../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
Itemsevent handler GC unsubscribe (folder watcher footgun) — must holdItemsrefs in static field. - IMAP
PR_INTERNET_MESSAGE_IDrace on Sent Items — use UserProperty GUID + 60s watcher fallback. - Outlook HTML quirks (VML, conditional comments) — send both plain
BodyandHTMLBody, 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.