Self-contained briefing for a fresh Claude Code session picking up work on this project from a Windows machine running VS 2022. Covers: - TL;DR + 4 features - MailRouter API contract + EspoCRM endpoints - Tech stack (locked decisions) - Repo layout + current state - Step-by-step recipe to regenerate the VSTO host project from the template (the hand-written csproj is incompatible with VS) - Conventions + deferred work + open risks Read this before doing anything: docs/PROJECT-BRIEFING.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 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.
Things deliberately deferred
| What | When | Why deferred |
|---|---|---|
| Windows CI runner on Coolify | Until ClickOnce publish becomes useful | Local builds on dev's VS 2022 suffice through week 5 |
| Code signing cert (self-signed) + GPO push to 10 PCs | Week 6 (before release) | IT task; cert isn't useful until we have something to sign |
ClickOnce hosting setup at platform.dev.marcus-law.co.il/outlook-addin/ |
Week 6 | No build artifacts yet |
| Mattermost release notification webhook in CI | Week 6 | Coupled to CI runner |
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.