A VSTO Outlook Add-in (C# / .NET Framework 4.8) that exports email threads to self-contained Markdown files. Inline images are embedded as Base64 data URIs, quoted reply blocks are stripped, and the entire thread is written as a single UTF-8 .md file.
- Export Email — exports the currently selected or open email as a standalone Markdown document.
- Export Thread — collects every message in the conversation via the Outlook Conversation API and exports the full thread, newest message first, matching Outlook's default sort order.
- Split-button ribbon control — appears in the Export group on the Home tab (after the Respond group) in both the folder view and the message inspector window.
- Inline image embedding —
cid:image references are resolved from inline attachments and replaced withdata:image/...;base64,...URIs so the exported file is fully self-contained. - Office HTML sanitisation — Office namespace tags (
<o:p>,<w:...>,<m:...>) are removed before HTML-to-Markdown conversion. - Quoted reply stripping — Outlook quoted-reply sections are removed before conversion so thread emails are not duplicated. Three Outlook reply patterns are handled:
<div id="divRplyFwdMsg">(modern Outlook)<div style="border-top ...">containing aFrom:header (older Outlook)<hr>followed by an inline boldFrom:/Sent:block (plain-text style replies)<blockquote>elements (forwarded or quoted content)
- Plain-text fallback — if HTML conversion produces empty output, the add-in falls back to
MailItem.Body(plain text). - Safe file naming — the email subject is sanitised (invalid filename characters replaced with
_, truncated to 100 characters) and used as the default filename in the Save dialog.
Each email in the exported file renders as the block below. A top-level # Subject heading precedes the first block.
# Project Update
---
## From: Jane Smith <jane@example.com>
**Date:** 2026-05-20 14:32
**To:** John Doe <john@example.com>
**CC:** team@example.com
**Subject:** Re: Project Update
Email body text converted from HTML.
The CC: line is omitted when the field is empty. Thread emails are separated by --- horizontal rules.
| Requirement | Details |
|---|---|
| Operating system | Windows 10 or Windows 11 |
| Outlook | Outlook 2016, 2019, 2021, or Microsoft 365 Desktop (Windows) |
| .NET | .NET Framework 4.8 (included with Windows 10 1903 and later) |
| Development only | Visual Studio 18 (2025 Insiders) with the Office/SharePoint development workload |
The dotnet CLI cannot build VSTO projects. MSBuild via Visual Studio is required.
Close Outlook before running the installer.
# Run from the repository root in an unrestricted PowerShell session
.\Install.ps1The script performs four steps:
- Builds the Release configuration using MSBuild (located automatically under
C:\Program Files\Microsoft Visual Studio). - Copies
OutlookExportMarkdown.dll,OutlookExportMarkdown.dll.manifest,OutlookExportMarkdown.vsto,HtmlAgilityPack.dll,ReverseMarkdown.dll, and the two VSTO utility DLLs to%APPDATA%\OutlookExportMarkdown. - Writes the add-in registration keys to
HKCU\SOFTWARE\Microsoft\Office\Outlook\Addins\OutlookExportMarkdownwithLoadBehaviorset to3(load at startup). - Adds a VSTO trust entry under
HKCU\SOFTWARE\Microsoft\VSTO\Security\Inclusionso Outlook loads the unsigned manifest without a prompt.
After installation completes, restart Outlook. The Export group appears on the Home tab in the ribbon.
.\Uninstall.ps1Removes the registry add-in entry, the VSTO trust entry, and the %APPDATA%\OutlookExportMarkdown folder. Restart Outlook after uninstalling.
- Select or open an email in Outlook.
- On the Home tab, click the Export Email split-button in the Export group.
- Choose a save location in the dialog. The email subject is pre-filled as the filename.
- Select or open any email in the conversation.
- Click the dropdown arrow on the split-button, then choose Export Thread.
- Choose a save location. All messages in the thread are collected and written to a single
.mdfile, newest first.
The Export Thread menu item is disabled (greyed out) when no email is selected. The button state re-evaluates on every selection change.
OutlookExportMarkdown/
├── OutlookExportMarkdown.sln
├── Install.ps1 — one-command installer
├── Uninstall.ps1 — one-command uninstaller
│
├── OutlookExportMarkdown/ — VSTO add-in project (.NET Framework 4.8)
│ ├── OutlookExportMarkdown.csproj
│ ├── ThisAddIn.cs — VSTO entry point; wires ribbon via
│ │ CreateRibbonExtensibilityObject
│ ├── ThisAddIn.Designer.cs — VSTO-generated partial class (do not edit)
│ ├── ExportRibbon.cs — IRibbonExtensibility callbacks
│ ├── ExportRibbon.xml — CustomUI XML (Embedded Resource)
│ ├── Models/
│ │ ├── EmailData.cs — plain data model (no COM types)
│ │ └── AttachmentData.cs — inline image bytes and MIME type
│ ├── Core/
│ │ ├── ConversationCollector.cs — Outlook COM → List<EmailData>
│ │ ├── EmailToMarkdown.cs — List<EmailData> → Markdown string
│ │ ├── ImageExtractor.cs — List<AttachmentData> → content-id → data URI map
│ │ └── FileExporter.cs — SaveFileDialog + UTF-8 file write
│ └── Properties/
│ └── AssemblyInfo.cs
│
└── OutlookExportMarkdown.Tests/ — MSTest unit test project (.NET Framework 4.8)
├── OutlookExportMarkdown.Tests.csproj
└── Core/
├── EmailToMarkdownTests.cs — 14 tests
└── ImageExtractorTests.cs — 5 tests
The add-in is structured in three layers.
┌──────────────────────────────────────────────┐
│ Outlook Integration Layer │
│ ExportRibbon.cs / ExportRibbon.xml │
│ ThisAddIn.cs │
├──────────────────────────────────────────────┤
│ Conversion Engine │
│ ConversationCollector EmailToMarkdown │
│ ImageExtractor │
├──────────────────────────────────────────────┤
│ Export Layer │
│ FileExporter (SaveFileDialog, UTF-8 write) │
└──────────────────────────────────────────────┘
Outlook Integration Layer — ExportRibbon.cs implements IRibbonExtensibility and loads ribbon XML from the embedded resource ExportRibbon.xml. Ribbon callbacks retrieve the selected MailItem from either the active Explorer (folder view) or the active Inspector (message window). ThisAddIn.cs returns the ribbon instance from CreateRibbonExtensibilityObject.
Conversion Engine — ConversationCollector calls MailItem.GetConversation(), walks the Conversation.GetTable() row set, retrieves each MailItem by EntryID, and returns a List<EmailData> sorted by SentOn descending. EmailToMarkdown parses the HTML body via HtmlAgilityPack, strips Office namespace nodes and quoted-reply sections, resolves cid: image references via ImageExtractor, then converts the sanitised HTML to Markdown using ReverseMarkdown.
Export Layer — FileExporter presents a SaveFileDialog with the sanitised subject as the default filename, then writes the Markdown string as UTF-8. Write errors are reported via MessageBox.
The data models (EmailData, AttachmentData) contain no COM types, which is what makes the conversion engine fully unit-testable without a live Outlook instance.
The project overrides the VSTO FindRibbons MSBuild task with a no-op. The task ordinarily loads the add-in DLL via Assembly.Load to discover designer-based ribbons; this fails in MSBuild CLI builds. Because the add-in uses an XML ribbon (IRibbonExtensibility), RibbonTypes is always empty and the no-op is safe.
Open OutlookExportMarkdown.sln in Visual Studio 18 (2025 Insiders) and press Ctrl+Shift+B, or invoke MSBuild directly:
& "C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe" `
OutlookExportMarkdown\OutlookExportMarkdown.csproj `
/p:Configuration=Release /verbosity:minimalThe dotnet build command does not support the VSTO project type ({BAA0C2D2-18E2-41B9-852F-F413020CAA33}). Use MSBuild only.
Press F5 in Visual Studio. VSTO launches Outlook with the add-in loaded from the Debug output folder. The DebugInfoExeName project property points to outlook.exe under HKLM\SOFTWARE\Microsoft\Office\16.0\Outlook\InstallRoot\Path.
Build the test project first (Debug configuration), then run:
& "C:\Program Files\Microsoft Visual Studio\18\Insiders\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" `
OutlookExportMarkdown.Tests\bin\Debug\OutlookExportMarkdown.Tests.dll15 of 19 tests execute via the CLI. The remaining 4 tests require additional context that is only available inside the Visual Studio Test Explorer runner. To run all 19 tests, use Test → Run All Tests in Visual Studio.
Test coverage:
| Class | Tests |
|---|---|
EmailToMarkdown |
14 |
ImageExtractor |
5 |
| Total | 19 |
ConversationCollector and FileExporter depend on the Outlook COM object model and System.Windows.Forms.SaveFileDialog respectively; they are verified by manual smoke test rather than automated unit tests.
| Package | Version | Purpose |
|---|---|---|
| HtmlAgilityPack | 1.11.61 | Parse Outlook HTML bodies into a traversable DOM |
| ReverseMarkdown | 4.6.0 | Convert sanitised HTML nodes to Markdown |
| MSTest.TestFramework | 3.1.1 | Unit test framework |
| MSTest.TestAdapter | 3.1.1 | VSTest adapter for MSTest |
| Scenario | Behaviour |
|---|---|
| No email selected | Export Thread menu item is disabled; re-evaluated on SelectionChange |
| Email has no conversation | ConversationCollector falls back to exporting the single MailItem |
| Inline image extraction fails | ![image unavailable]() placeholder is inserted; conversion continues |
| HTML conversion returns empty output | Falls back to MailItem.Body (plain text) |
| Save dialog cancelled | Export aborts silently; no file is written |
| File write error | MessageBox shown with the attempted path and exception message |
- Windows only. Outlook on Mac, Outlook Web, and the new Outlook for Windows (web-based) are not supported.
- Non-inline file attachments (PDFs, Office documents, etc.) are not exported.
- Batch export of multiple unrelated threads in a single operation is not supported.
- The installer uses a self-signed temporary key (
OutlookExportMarkdown_TemporaryKey.pfx). Enterprise deployments may require a code-signing certificate trusted by the organisation's Group Policy.
No license file is present in this repository. All rights reserved by the author.