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, daily rolling, last 7 files retained) - Seq sink (address and optional API key from config)
- logs are flushed via
Log.CloseAndFlushAsync()in afinallyblock at the end of every run
- rolling local file logs (
- Reads the input path either:
- from the first CLI argument, or
- from console prompt input.
- Validates that the path is an existing file or directory:
- If it is a single
.emlfile, that file is processed. - If it is a directory, every top-level
*.emlfile in it is processed (non-recursive). A failure on one file is logged and counted but does not stop the rest of the batch.
- If it is a single
- For each file, 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). - When the run finishes, writes a summary log entry with the input type (file/directory), total elapsed processing time, and the number of files processed, succeeded, and failed.
Download the latest release package. It contains the executable and appsettings.json.
Run one of:
-
Interactive mode:
EML2PDF.exe
-
Direct argument mode (single file):
EML2PDF.exe "C:\path\to\mail.eml" -
Direct argument mode (directory — processes every top-level
.emlfile):EML2PDF.exe "C:\path\to\folder"
appsettings.json keys currently used by the app:
Seq:ServerAddress: Seq endpoint URL.Seq:ApiKey: API key used to authenticate with Seq. Leave empty to send events without authentication.
Example:
{
"Seq": {
"ServerAddress": "http://localhost:5341/",
"ApiKey": ""
}
}- Success output: a line prefixed with
RET-OUTPUT: <pdfPath>. - When multiple PDF attachments are extracted, one
RET-OUTPUT:line is printed per file. - On completion a summary line is printed to the console and logged, e.g.
Done. Processed 5 file(s) in 00:00:12.3456789. Succeeded: 4, Failed: 1. - Exit code
0: all processed files succeeded (or their output already existed). - Exit code
1: invalid input path, at least one file failed, or a runtime error occurred.
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