Skip to content

Commit

Permalink
First basic version
Browse files Browse the repository at this point in the history
  • Loading branch information
baltpeter committed Mar 24, 2021
1 parent fb7cfa6 commit 32ed542
Show file tree
Hide file tree
Showing 6 changed files with 647 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
tmp/
*.tmp
tmp.*
*.tmp.*

dist/

Expand Down
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "mail2pdf",
"version": "1.0.0",
"version": "0.0.0",
"description": "Render emails stored as .eml files to PDF.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "Benjamin Altpeter <hi@bn.al>",
"repository": "https://github.com/baltpeter/mail2pdf",
"license": "MIT",
"devDependencies": {
"@types/mailparser": "^3.0.2",
"@types/node": "^14.14.20",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
Expand All @@ -23,7 +24,8 @@
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"start": "node dist/index.js"
"start": "node dist/index.js",
"test": "echo 'TODO: No test specified yet.'"
},
"husky": {
"hooks": {
Expand All @@ -37,5 +39,11 @@
"*.{ts,js}": [
"eslint --fix"
]
},
"dependencies": {
"handlebars": "^4.7.7",
"mailparser": "^3.1.0",
"pretty-bytes": "^5.6.0",
"puppeteer": "^8.0.0"
}
}
3 changes: 0 additions & 3 deletions src/demo.ts

This file was deleted.

75 changes: 74 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,74 @@
export { demo } from './demo';
import { promises as fs } from 'fs';
import { join } from 'path';
import { simpleParser, AddressObject } from 'mailparser';
import puppeteer from 'puppeteer';
import handlebars from 'handlebars';
import prettyBytes from 'pretty-bytes';
import StreamModule = require('stream');
import Stream = StreamModule.Stream;

type Template = 'thunderbird';
type Language = 'en';

// The header and footer templates don't respect the page styles. They need their own styles, otherwise they will be
// tiny, see: https://github.com/puppeteer/puppeteer/issues/1822#issuecomment-530533300
const headerFooter = (html: string, align = 'left') =>
`<div style="font-size: 10px; margin: 10px 20px; width: 100%; text-align: ${align}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${html}</div>`;
const addressesToString = (addr?: AddressObject | AddressObject[]) =>
addr instanceof Array ? addr.reduce((acc, cur) => acc + ', ' + cur.text, '') : addr?.text;

/**
* Generate a PDF from the provided .eml file.
*
* @param eml The email in .eml format to process. Either provide a file path as a string, which will then be read from
* the filesystem, or the content of the .eml file as a `Buffer` or `Stream`.
* @param options.out_path The file path where the generated PDF should be stored (optional).
* @param options.language The language to be used for the labels in the generated PDF (optional, defaults to `en`).
* Currently, only English is supported.
* @param options.template_name The template to use (optional, defaults to `thunderbird`). Currently, only `thunderbird`
* is supported which will generate output similar to Thunderbird’s “Print to PDF” feature.
*
* @returns A `Buffer` containing the generated PDF.
*/
export default async function mail2pdf(
eml: string | Buffer | Stream,
options?: { out_path?: string; language?: Language; template_name?: Template }
): Promise<Buffer> {
options = { language: 'en', template_name: 'thunderbird', ...options };

const mail = await simpleParser(typeof eml === 'string' ? await fs.readFile(eml) : eml);

const template = handlebars.compile(
(await fs.readFile(join(__dirname, '..', 'templates', options.template_name + '.hbr'))).toString()
);
const html = template({
subject: mail.subject,
has_html_body: mail.html !== false,
body: mail.html || mail.textAsHtml,
from: mail.from?.text,
date: mail.date?.toISOString(),
to: addressesToString(mail.to),
cc: addressesToString(mail.cc),
bcc: addressesToString(mail.bcc),
priority: mail.priority,
attachments: mail.attachments
.filter((a) => a.contentDisposition === 'attachment')
.map((a) => ({ ...a, filename: a.filename || '<unnamed>', prettySize: prettyBytes(a.size) })),
});

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setContent(html);
const pdf = await page.pdf({
format: 'a4',
margin: { top: '20mm', bottom: '20mm', right: '20mm', left: '20mm' },
displayHeaderFooter: true,
headerTemplate: headerFooter('<span class="title"></span>'),
footerTemplate: headerFooter('<span class="pageNumber"></span>/<span class="totalPages"></span>', 'right'),
printBackground: true,
...(options.out_path ? { path: options.out_path } : {}),
});
await browser.close();

return pdf;
}
119 changes: 119 additions & 0 deletions templates/thunderbird.hbr
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<html>
<head>
<title>{{subject}}</title>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
font-size: 16px;
line-height: 1.4;
background-color: transparent !important;
}

* {
/*
Outlook inserts `page: <something>;` on certain elements which causes Chrome to push them onto a separate
page. This rule disables this as the additional pages contain mostly unnecessary whitespace and Outlook
doesn't even respect those rules itself when printing.
*/
page: unset !important;
}

a {
color: rgb(11, 108, 218);
}

#header {
margin-bottom: 25px;
}

#attachments {
margin: 0 15px 0 10px;
}

#attachments h2 {
font-weight: normal;
font-size: 9pt;
border-bottom: 1px solid rgb(87, 87, 87);
line-height: 0;
margin: 10px -15px 15px -10px;
}

#attachments h2 span {
background-color: #fff;
margin-left: 10px;
}

table#attachments-table {
width: 100%;
border-collapse: collapse;
}

#attachments-table td:nth-of-type(2) {
text-align: right;
}

table#attachments-table tr td {
border-bottom: 1px solid rgb(87, 87, 87);
padding: 5px 0;
}

table#attachments-table tr:last-child td {
border: none;
}

ul.br-list {
margin: 0;
padding: 0;
list-style: none;
}

.mono {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.8em;
}
</style>
</head>
<body>
<div id="header">
<ul class="br-list">
<li><strong>Subject:</strong> {{subject}}</li>
{{#if from}}
<li><strong>From:</strong> {{from}}</li>
{{/if}}
{{#if date}}
<li><strong>Date:</strong> {{date}}</li>
{{/if}}
{{#if to}}
<li><strong>To:</strong> {{to}}</li>
{{/if}}
{{#if cc}}
<li><strong>CC:</strong> {{cc}}</li>
{{/if}}
{{#if bcc}}
<li><strong>BCC:</strong> {{bcc}}</li>
{{/if}}
{{#if priority}}
<li><strong>Priority:</strong> {{priority}}</li>
{{/if}}
</ul>
</div>

<div id="body" {{#unless has_html_body}}class="mono" {{/unless}}>
{{{body}}}
</div>
{{#if attachments}}
<div id="attachments">
<h2><span>Attachments:</span></h2>
<table id="attachments-table">
{{#each attachments}}
<tr>
<td>{{this.filename}}</td>
<td>{{this.prettySize}}</td>
</tr>
{{/each}}
</table>
</div>
{{/if}}
</body>
</html>
Loading

0 comments on commit 32ed542

Please sign in to comment.