EML2PDF is a .NET CLI utility that converts an email file (.eml) into a PDF.
At runtime, the app:
- Loads settings from
appsettings.jsonlocated next to the executable. - Configures Serilog with:
- rolling local file logs (
logs/log-*.txt) - Seq sink (address from config)
- rolling local file logs (
- Reads the input path either:
- from the first CLI argument, or
- from console prompt input.
- Validates that the file exists.
- Searches for PDF attachments at the deepest nesting level:
- If any PDF attachments are found, all of them are extracted and saved to the same folder, using each attachment's original filename.
- If no PDF attachments are found, the deepest nested
.emlbody is rendered via headless Chromium (PuppeteerSharp) and exported to PDF as<originalName>.eml.pdf.
- Writes the output in the same folder as input using
<originalName>.eml.pdf. - If successful, renames the source
.emlto<yyyyMMddHHmmss> - <name>.eml.bakin the same folder (UTC timestamp, no spaces, seconds precision).
Download the latest release package. It contains the executable and appsettings.json.
Run one of:
-
Interactive mode:
EML2PDF.exe
-
Direct argument mode:
EML2PDF.exe "C:\path\to\mail.eml"
appsettings.json keys currently used by the app:
Seq:ServerAddress: Seq endpoint URL.Seq:AppName: value added as Serilog property.
Example:
{
"Seq": {
"ServerAddress": "http://localhost:5341/",
"AppName": "EML2PDF"
}
}- Success output: a line prefixed with
RET-OUTPUT: <pdfPath>. - Exit code
0: conversion/extraction succeeded (or all output files already existed). - When multiple PDF attachments are extracted, one
RET-OUTPUT:line is printed per file. - Exit code
1: invalid input path or runtime error.
The following issues are currently present in code and should be considered known limitations:
Startup errors are not globally guarded.Fixed: all ofMainis wrapped in a singletry/catch(Exception)/finally;CloseAndFlushAsyncis called unconditionally in thefinallyblock.- Seq sink is always enabled when logger is built.
- Bad/missing Seq endpoint can cause logging pipeline failures depending on environment/config.
BrowserFetcher().DownloadAsync()runs for every conversion.- Network or filesystem failures can break processing; cold-start latency is high.
- Output file name is fixed to
<input>.eml.pdf.- Existing file causes an early return instead of deterministic overwrite/versioning policy.
- Source file cleanup (delete/move) is not wrapped in local error handling.
- Permission/lock issues can fail after a successful conversion.
- Backup naming has minute-level timestamp resolution.
- Multiple runs in same minute can collide on
.bakname.
- Multiple runs in same minute can collide on
- MIME traversal/decoding assumes all nested payloads are valid.
- Corrupt nested
.emlor malformed parts can throw and abort.
- Corrupt nested
- HTML rendering does not sanitize/contain remote resources.
- Embedded/linked assets may fail to load or behave inconsistently in headless rendering.
- PDF page sizing uses
document.body.scrollHeightas a single page height.- Very long messages can create oversized single-page PDFs and stress rendering.
- No cancellation or timeout boundaries.
- Stalls in MIME parsing, browser startup, network download, or PDF generation can hang the process.
- .NET 10
- MimeKit
- PuppeteerSharp (Chromium download and headless rendering)
- Serilog + File sink + Seq sink