This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
dotnet build does not support VSTO projects. Use MSBuild from Visual Studio 18 (2025 Insiders) only.
# Add-in project (Debug)
& "C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe" `
OutlookExportMarkdown\OutlookExportMarkdown.csproj /p:Configuration=Debug /verbosity:minimal
# Test project (builds both projects — test project references add-in)
& "C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe" `
OutlookExportMarkdown.Tests\OutlookExportMarkdown.Tests.csproj /p:Configuration=Debug /verbosity:minimal
# Release build (used by Install.ps1)
& "C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe" `
OutlookExportMarkdown\OutlookExportMarkdown.csproj /p:Configuration=Release /verbosity:minimal# Run all tests
$vstest = "C:\Program Files\Microsoft Visual Studio\18\Insiders\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe"
& $vstest OutlookExportMarkdown.Tests\bin\Debug\OutlookExportMarkdown.Tests.dll
# Run a single test class
& $vstest OutlookExportMarkdown.Tests\bin\Debug\OutlookExportMarkdown.Tests.dll /TestCaseFilter:"ClassName=EmailToMarkdownTests"
# Run a single test by name
& $vstest OutlookExportMarkdown.Tests\bin\Debug\OutlookExportMarkdown.Tests.dll /TestCaseFilter:"Name=Convert_SingleEmail_StartsWithH1Subject"19 tests total (14 EmailToMarkdown, 5 ImageExtractor). All 19 pass via vstest CLI.
ConversationCollector and FileExporter are not unit-tested — they depend on Outlook COM and SaveFileDialog respectively. Verify them via manual smoke test.
# Full permanent install (builds Release, copies to %AppData%, writes registry)
powershell -ExecutionPolicy Bypass -File Install.ps1
# Register debug build (for development iteration without reinstalling)
& "...\MSBuild.exe" OutlookExportMarkdown\OutlookExportMarkdown.csproj /p:Configuration=Debug /t:RegisterOfficeAddin /verbosity:minimal
# Restart Outlook to pick up DLL changes
Stop-Process -Name OUTLOOK -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Start-Process "C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE"Three layers with a hard boundary at the COM/non-COM interface:
ExportRibbon.cs / ExportRibbon.xml ← IRibbonExtensibility; reads MailItem from Explorer/Inspector
│
ConversationCollector.cs ← Outlook COM → List<EmailData> (COM boundary)
│
EmailToMarkdown.cs ← List<EmailData> → Markdown string
ImageExtractor.cs ← List<AttachmentData> → contentId → data URI map
│
FileExporter.cs ← SaveFileDialog + UTF-8 write
The COM boundary is ConversationCollector. Everything below it (EmailToMarkdown, ImageExtractor, FileExporter, EmailData, AttachmentData) has no Outlook COM types, which is what makes those classes unit-testable.
ExportRibbon.xml (Embedded Resource) — loaded at runtime via Assembly.GetManifestResourceStream. The resource name is OutlookExportMarkdown.ExportRibbon.xml (namespace + filename). Any change to the XML requires a rebuild and Outlook restart to take effect.
FindRibbons no-op override — the VSTO FindRibbons MSBuild task tries to load the add-in DLL via Assembly.Load(name) to discover designer-based ribbon classes. This fails in CLI builds because NuGet/VSTO reference assemblies aren't in the MSBuild AppDomain probing path. Since the add-in uses XML ribbon (IRibbonExtensibility), not the Ribbon Designer, the task is overridden with an inline CodeTaskFactory no-op that returns an empty RibbonTypes array. This override must appear before the VSTO targets import in the .csproj.
VSTO targets path — VS 18 (2025 Insiders) moved the VSTO targets from the historical VSTO\Microsoft.Office.Tools.targets path to OfficeTools\Microsoft.VisualStudio.Tools.Office.targets. The .csproj uses $(VSToolsPath)\OfficeTools\... which resolves correctly for VS 18 and is set with a fallback VisualStudioVersion=18.0.
Conditional compilation for VSTO files — ThisAddIn.cs, ThisAddIn.Designer.cs, and ExportRibbon.cs are compiled only when the VSTO targets file exists (controlled by Condition="'$(VstoAvailable)' == 'true'" on the <Compile> items). This allows the test project's dependency chain to build on machines without VSTO, since Core/ and Models/ have no VSTO references.
OutlookAddInBase base class — VS 18 VSTO uses Microsoft.Office.Tools.Outlook.OutlookAddInBase (from Microsoft.Office.Tools.Outlook.v4.0.Utilities.dll) as the ThisAddIn base class, replacing the older FormRegionCollection/RequestService pattern. ThisAddIn.Designer.cs is generated from the blueprint in ThisAddIn.Designer.xml.
ConvertBody applies these transformations in order before calling ReverseMarkdown.Converter.Convert():
- Quoted-reply stripping (
StripOutlookQuotedReply) — removes the quoted original email that Outlook inlines into replies:<div id="divRplyFwdMsg">+ preceding<hr>(modern Outlook)<div style="border-top ...">starting with "From:" text (older Outlook)<blockquote>elements (forwarded content)<hr>followed by a sibling starting with "From:" text (plain-text replies)
- Office namespace tags — removes any element whose tag name contains
:(e.g.<o:p>,<w:sdtPr>) - Post-conversion regex —
StripMarkdownQuotedHeaderremoves any**From:**/**Sent:**block that survived HTML stripping (inline bold text not wrapped in a div)
| Callback | When used |
|---|---|
OnExportSingleEmail |
Split-button primary click; exports only the active single email |
OnExportThread |
Split-button dropdown → Export Thread; exports full conversation |
GetExportEnabled |
Returns true when ActiveExplorer().Selection[1] is a MailItem |
GetInspectorEnabled |
Returns true when ActiveInspector().CurrentItem is a MailItem |
GetCurrentMailItem() checks the active Inspector first (message opened in its own window), falls back to GetSelectedMailItem() (reading pane selection in Explorer).
- The
.csproj.userfile setsStartAction=Program/StartProgram=OUTLOOK.EXEso VS F5 launches Outlook. Without this file VS shows "startup project cannot be launched". - The self-signed dev certificate (
OutlookExportMarkdown_TemporaryKey.pfx) is required for ClickOnce manifest signing (<SignManifests>true). It is committed intentionally — it is a dev-only key with no trusted authority. HKCU\Software\Microsoft\Office\16.0\Outlook\InstallRoot\Pathmust be set for the VSTODebugInfoExeNameregistry-path resolution to findoutlook.exe.Install.ps1sets this.- Outlook must be closed before running
Install.ps1orRegisterOfficeAddin.