From cd0d25593243861d3ca67b09a6ec86eea2ee64e3 Mon Sep 17 00:00:00 2001 From: Jan Hapke Date: Thu, 4 Jun 2026 23:04:52 +0200 Subject: [PATCH 01/18] chore(export): install turndown dependencies --- package-lock.json | 35 +++++++++++++++++++++++++++++++++++ package.json | 3 +++ 2 files changed, 38 insertions(+) diff --git a/package-lock.json b/package-lock.json index 94f8c84..fd35085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,8 @@ "mailparser": "^3.9.8", "simple-git": "3.36.0", "slugify": "^1.6.9", + "turndown": "^7.2.4", + "turndown-plugin-gfm": "^1.0.2", "zod": "^4.3.6" }, "bin": { @@ -26,6 +28,7 @@ "devDependencies": { "@types/mailparser": "^3.4.6", "@types/node": "25.6.0", + "@types/turndown": "^5.0.6", "@vitest/coverage-v8": "^4.1.5", "tsx": "4.21.0", "typescript": "6.0.3", @@ -938,6 +941,12 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@napi-rs/keyring": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", @@ -1562,6 +1571,13 @@ "undici-types": "~7.19.0" } }, + "node_modules/@types/turndown": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", + "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", @@ -3103,6 +3119,25 @@ "fsevents": "~2.3.3" } }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/turndown-plugin-gfm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz", + "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==", + "license": "MIT" + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", diff --git a/package.json b/package.json index 5c3fe26..300c6f6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "devDependencies": { "@types/mailparser": "^3.4.6", "@types/node": "25.6.0", + "@types/turndown": "^5.0.6", "@vitest/coverage-v8": "^4.1.5", "tsx": "4.21.0", "typescript": "6.0.3", @@ -63,6 +64,8 @@ "mailparser": "^3.9.8", "simple-git": "3.36.0", "slugify": "^1.6.9", + "turndown": "^7.2.4", + "turndown-plugin-gfm": "^1.0.2", "zod": "^4.3.6" } } From 91ed2a04487d869037938298505de5f1642c2631 Mon Sep 17 00:00:00 2001 From: Jan Hapke Date: Thu, 4 Jun 2026 23:05:24 +0200 Subject: [PATCH 02/18] refactor(sync): export parseHeaderBlock and extractRawHeader --- src/core/index.ts | 2 +- src/core/sync.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index d3ca834..f60d7b5 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -11,7 +11,7 @@ export { findRepository } from './discovery.js' // Sync export type { SyncResult, SyncOptions, FolderSyncResult } from './sync.js' -export { syncAccount, reindexLocalFolders, decodeMimeWords } from './sync.js' +export { syncAccount, reindexLocalFolders, decodeMimeWords, parseHeaderBlock, extractRawHeader } from './sync.js' // Browse export type { MessageSummary } from './browse.js' diff --git a/src/core/sync.ts b/src/core/sync.ts index 37dad9c..b68b5cf 100644 --- a/src/core/sync.ts +++ b/src/core/sync.ts @@ -113,13 +113,13 @@ export function decodeMimeWords(value: string): string { } // Extract the first occurrence of a named header from the raw (unfolded) header block. -function extractRawHeader(unfolded: string, name: string): string { +export function extractRawHeader(unfolded: string, name: string): string { const re = new RegExp(`^${name}:[ \\t]*(.*)$`, 'im') return unfolded.match(re)?.[1]?.trim() ?? '' } // Unfold and extract just the header section from raw email bytes. -function parseHeaderBlock(rawSource: Buffer | string): string { +export function parseHeaderBlock(rawSource: Buffer | string): string { const text = typeof rawSource === 'string' ? rawSource.slice(0, 65536) : rawSource.subarray(0, 65536).toString('utf-8') From 154bae5beba2f135bbe8a5d4853804008ac8b654 Mon Sep 17 00:00:00 2001 From: Jan Hapke Date: Thu, 4 Jun 2026 23:12:20 +0200 Subject: [PATCH 03/18] feat(export): conversion helpers and frontmatter builder --- src/core/export.ts | 304 +++++++++++++++++++++++++++++ src/types/turndown-plugin-gfm.d.ts | 6 + tests/unit/export.test.ts | 244 +++++++++++++++++++++++ 3 files changed, 554 insertions(+) create mode 100644 src/core/export.ts create mode 100644 src/types/turndown-plugin-gfm.d.ts create mode 100644 tests/unit/export.test.ts diff --git a/src/core/export.ts b/src/core/export.ts new file mode 100644 index 0000000..0e04fec --- /dev/null +++ b/src/core/export.ts @@ -0,0 +1,304 @@ +// src/core/export.ts — no exit calls, no console, no CLI imports. +import fs from 'node:fs/promises' +import path from 'node:path' +import { simpleParser } from 'mailparser' +import type { ParsedMail } from 'mailparser' +import TurndownService from 'turndown' +import { gfm } from 'turndown-plugin-gfm' +import { parseHeaderBlock, extractRawHeader } from './sync.js' +import { listFolders } from './browse.js' + +// ── Types ───────────────────────────────────────────────────────────────────── + +export interface ExportOptions { + excludeFolders?: string[] + onlyFolders?: string[] + verbose?: boolean + onLog?: (msg: string) => void +} + +export interface ExportResult { + exported: number + errors: number + folderResults: FolderExportResult[] +} + +export interface FolderExportResult { + path: string + exported: number + error?: Error +} + +// ── HTML → Markdown ─────────────────────────────────────────────────────────── + +export function preprocessHtml(html: string): string { + // Strip

Hello

' + expect(preprocessHtml(html)).not.toContain('