Skip to content

Latest commit

 

History

History
113 lines (79 loc) · 7.09 KB

File metadata and controls

113 lines (79 loc) · 7.09 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build

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

Tests

# 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.

Install / reload Outlook

# 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"

Architecture

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.

Key design decisions

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 filesThisAddIn.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.

HTML preprocessing pipeline in EmailToMarkdown

ConvertBody applies these transformations in order before calling ReverseMarkdown.Converter.Convert():

  1. 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)
  2. Office namespace tags — removes any element whose tag name contains : (e.g. <o:p>, <w:sdtPr>)
  3. Post-conversion regexStripMarkdownQuotedHeader removes any **From:**/**Sent:** block that survived HTML stripping (inline bold text not wrapped in a div)

Ribbon callbacks reference

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

VSTO project quirks

  • The .csproj.user file sets StartAction=Program / StartProgram=OUTLOOK.EXE so 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\Path must be set for the VSTO DebugInfoExeName registry-path resolution to find outlook.exe. Install.ps1 sets this.
  • Outlook must be closed before running Install.ps1 or RegisterOfficeAddin.