npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

trogon

v1.0.2

Published

Trogon is a util that prepares HTML content for email distribution. It can generate MIME/EML files suitable for a wide range of email clients, with the focus on compatibility in older email clients, like desktop Outlook.

Downloads

18

Readme

🦜Trogon

Trogon is a util that prepares HTML content for email distribution. It can generate MIME/EML files suitable for a wide range of email clients, with the focus on compatibility in older email clients, like desktop Outlook.

Table of Contents

Motivation

I had a trivial but rather an irritating problem of getting markup to render reliably across Gmail, Thunderbird, and Outlook. We needed to prepare emails based on a few sources (HTML generated with react-email, raw HTML+CSS, and HTML from email makers); what's worse, the client I was working with had some rules disallowing to send emails programmatically and requiring the sender to be from the corporate domain. So I ended up putting together a simple pipeline that I can pass all the required files through and apply some common transformations (like CSS inlining, embedding images, and comments removal). Then I would generate an EML file to send from a desktop Outlook client. Later still, that utility script became Trogon.

In case you don't know, trogons are a family of birds that includes genera of various trogons and quetzals. According to the Handbook of the Birds of the World, "apart from their great beauty, trogons are notorious for their lack of other immediately engaging qualities". In real life, a trogon would make for a needlessly fancy homing pigeon. And so is this library: a resplendent but ultimately a bit underwhelming solution to the problem.

Installation

With npm:

npm i trogon

Import using either ES modules:

import Trogon from "trogon";

Or with require:

const { Trogon } = require("trogon");

Usage

When creating a new instance of Trogon you can specify path to input and output files, as well as other configurations.

const trogon = new Trogon({
    inputHtmlPath: "./body.html",
    inputCssPath: "./styles.css",
});

Here's a basic example on how to use Trogon to load HTML from a file, embed images as base64, convert external stylesheet to inline styles, and prepare an email for sending:

await trogon
    .loadHtmlFromFile()
    .embedExternalImgAsBase64()
    .convertCssToInlineStyles()
    .composeMail()
    .markAsUnsent()
    .dumpEml();

All methods return the current instance of Trogon, allowing for chaining. Trogon's constructor returns an instance wrapped into a proxy, meaning it supports async chaining.

Cookbook

Here are some additional recipes I use Trogon for.

Loading HTML into a mail body from a string

await new Trogon()
    .loadHtmlFromString(html)
    .composeMail()
    .dumpEml();

Cleaning all comments from the HTML file and saving as a different HTML file

await new Trogon({ inputHtmlPath: "./body.html" })
    .loadHtmlFromFile()
    .cleanComments()
    .cleanConditionalComments()
    .cleanMsoConditions()
    .dumpHtml();

Sending a test email to my test email addresses via SMTP

await new Trogon(config)
    // ... load your HTML and perform required transformations
    .composeMail()
    .setSmtpCred({
        host: process.env.SMTP_HOST,
        port: process.env.SMTP_PORT,
        auth: {
            user: process.env.SMTP_AUTH_USER,
            pass: process.env.SMTP_AUTH_PASS,
        },
    })
    .inferSubjectFromTitle()
    .setRecipients(to)
    .setSender(from)
    .sendEmail();

Using trogon.state for Custom Operations

Trogon is rather limited in its scope; thankfully, it uses some common Node.js libraries, making it easy to take the current state of the instance and apply your own operations to it. Call the state prop to get the current state:

const state = await new Trogon()
    // ... load your HTML and perform required transformations
    .state;

Although it's a property, I recommend using getting it with await to make sure the proxy doesn't run into any race conditions and you don't get undefined.

State gives you access to the following properties:

  • $: An instance of the Cheerio API with the loaded HTML content.
  • mail: An object describing the email configuration options, based on the nodemailer Mail.Options type.
  • smtpTransport: An instance of a nodemailer SMTP transporter for sending emails.
  • subject: The subject line to be used in the email.
  • domain: The domain to use when constructing Message-ID.
  • logger: An instance of a bunyan logger used for logging information, warnings and errors.
  • logCacheStream: A custom stream object compatible with bunyan used for caching or storing logs.

You might have some extra transformation in mind to be performed in the Cheerio instance or even the mail object -- in those cases, it'll be probably just easier to use Cheerio or Nodemailer directly. However, if you still want to re-import some values into the state, it should in principle be possible due to how references work in JS. For example,

const trogon1 = await new Trogon(validConfig);
const stateToMutate1 = await trogon1.state;
stateToMutate1.subject = "Quetzal";
const currentState1 = await trogon1.state;
console.log(currentState1.subject);
// Prints out "Quetzal" because stateToMutate1 
// is a reference to the state object

const trogon2 = await new Trogon(validConfig);
const stateToMutate2 = { ...(await trogon2.state) };
stateToMutate2.subject = "Quetzal";
const currentState2 = await trogon2.state;
console.log(currentState2.subject);
// Prints out the default "No Subject" because 
// stateToMutate2 is a deep(ish) copy of the state object

API Reference

Configuration Object

Configuration object is used in the constructor, like so:

const trogon = new Trogon(config);

Or so:

const trogon = new Trogon({
    inputHtmlPath: "./in.css",
    inputCssPath: "./in.css",
    outputHtmlPath: "./out.html",
    outputEmlPath: "./out.eml",
    outputLogsPath: "./logs.json",
    errorMode: "log",
    timeout: 30000,
});

inputHtmlPath (string, optional)

The filesystem path to the HTML file you want to process. If not set, Trogon expects HTML to be loaded in a different manner (e.g., from a string).

inputCssPath (string, optional)

The filesystem path to an external CSS file containing styles to be applied to the HTML content. This is optional and only needed if you have separate CSS to inline.

outputHtmlPath (string, optional)

Where to save the resulting HTML file after processing. If not provided, the processed HTML will be saved to the default path ./out.html.

outputEmlPath (string, optional)

Where to save the EML file that is produced after processing. If not specified, the EML file will be generated at the default path ./out.eml.

outputLogsPath (string, optional)

Filesystem path where logs will be dumped when requested.

errorMode (string, optional)

Defines how errors are handled during processing. The modes are 'throw', which will throw exceptions, and 'log', which will log errors instead of throwing. Defaults to the "throw" mode.

timeout (number, optional)

The maximum time in milliseconds to wait before timing out an operation. Extend this if you have large files or operations that take more time. Default: 30000 (30 seconds).

HTML loading

trogon.loadHtmlFromFile()

This method synchronously reads HTML content from a file path specified in the inputHtmlPath property of the instance's configuration object. It then initialises Cheerio with the loaded HTML content, allowing for further DOM manipulation or querying using Cheerio's jQuery-like API.

trogon.loadHtmlFromString(htmlContent: string)

Parses HTML content from a string into the instance's state using Cheerio.

trogon.dumpHtml()

Saves the current state of the HTML document to a config.outputHtmlPath.

If the HTML content is not loaded into state.$, an error is thrown.

Manipulation of DOM, CSS, HTML Comments

await trogon.embedExternalImgAsBase64()

Converts and embeds all external image sources within the HTML document to Base64-encoded data URIs. Some email clients are configured so that the won't load an image from a src, and embedding is a way to avoid that.

By default, each request's timeout is set to 30000 ms. If you have a big images or if you're on an unstable or slow connection, you can increase the timeout by specifying timeout in the config when creating a Trogon instance.

If there is a problem with fetching or encoding any of the external images, an error is thrown.

trogon.convertCssToHeadStyleTag()

Copies the provided external stylesheet into the <style> tag in the head of the loaded HTML document.

If the HTML content is not loaded into state.$, an error is thrown.

If config.inputCssPath is not specified, an error is thrown.

trogon.convertInternalStylesheetToInlineStyles()

Transforms styles defined in internal stylesheets (<style> tags inside <head>) into inline styles within the HTML elements.

If the HTML content is not loaded into state.$, an error is thrown.

If config.inputCssPath is not specified, an error is thrown.

trogon.convertCssToInlineStyles()

Converts the provided external stylesheet into inline styles within the HTML elements.

If the HTML content is not loaded into state.$, an error is thrown.

If config.inputCssPath is not specified, an error is thrown.

trogon.cleanMsoConditions()

This method performs two main clean-up operations on the HTML content to improve email compatibility. It removes inline 'style' attributes that include Microsoft Outlook-specific 'mso' prefixed styles, which can cause inconsistent rendering in other email clients. Additionally, it removes HTML comments that utilise conditional 'mso' directives intended solely for Microsoft Outlook.

If the HTML content is not loaded into state.$, an error is thrown.

trogon.cleanConditionalComments()

Removes Internet Explorer (IE) specific conditional comments from an HTML document. Many email clients may not interpret causing undesired behaviours and making it render differently than expected.

If the HTML content is not loaded into state.$, an error is thrown.

trogon.cleanComments()

Removes all HTML comment nodes from the loaded HTML document.

If the HTML content is not loaded into state.$, an error is thrown.

Mail composition

trogon.inferSubjectFromTitle()

Extracts the text content of the HTML tag to use as the email subject.

If the HTML content is not loaded into state.$, an error is thrown.

trogon.setSubject(subject: string)

Assigns a user-defined subject to the instance's state for use in email construction.

trogon.composeMail()

Constructs an email-ready object, state.mail, which can be directly utilised by a mail transport mechanism or written as an EML file.

state.mail is initialised as an object compatible with the Mail.Options type of NodeMailer.

If the HTML content is not loaded into state.$, an error is thrown.

trogon.markAsUnsent()

Sets the X-Unsent header to 1.

The X-Unsent header in an EML file is typically used to indicate that the email should be opened in the email client as a draft, rather than as a received message.

If composeMail hadn't been called before, an error is thrown.

trogon.setDomain(domain: string)

Assigns a user-defined domain name to be used for Message-ID generation.

Throws an error if user-submitted domain is not a valid domain name.

trogon.generateMessageId()

Generates a Message-ID header for the mail message using specified domain.

According to email specifications (RFC 2822), every email should have a unique Message-ID value, which is used to identify each message across email systems. For example, Message-ID: 75581fbe-c6a6-3e1b-a268-cc3ac717ac00@localhost. The second half (after @) is the domain part, which should ideally be the sender's fully qualified domain name, but localhost is often used for emails generated on a local machine or when the generating system does not have a proper domain name configured.

Generating a Message-ID with a "good" domain name might be required when dealing with older or otherwise jury-rigged email servers that may try to only allow a white-listed domain but won't regenerate the Message-ID when you're sending the draft.

If setDomain hadn't been called, an error is thrown.

If composeMail hadn't been called before, an error is thrown.

trogon.cleanMessageId()

Removes the Message-ID header from the output EML file.

Generally speaking you don't need to clean it, as most mailing clients will overwrite it themselves. Certain mail servers may not do that and may further attempt to block your email if the Message-ID doesn't feature a white-listed domain name.

Since it updates the dumped EML file, this method doesn't work (or needed) for sending emails programmatically.

await trogon.dumpEml()

Generates a standard MIME structure (EML file) from the composed mail object and writes it to config.outputEmlPath.

If composeMail hadn't been called before, an error is thrown.

Mailing using SMTP (for testing or distribution)

trogon.setSmtpCred(transportOptions: SMTPTransport.Options)

Configures the SMTP transport mechanism with the provided credentials and options for sending emails programmatically through the Trogon instance.

transportOptions should conform to the SMTPTransport.Options interface from the Nodemailer library.

trogon.setSender(from: string)

Sets the sender for the composed mail object.

If composeMail hadn't been called before, an error is thrown.

trogon.setRecipients(to: string | string[])

Sets the recipients for the composed mail object.

If composeMail hadn't been called before, an error is thrown.

trogon.sendEmail()

Sends the composed email using the configured SMTP transport settings (via Nodemailer).

Errors may be thrown to report missing SMTP configuration, email composition data, a lack of sender or recipient information, or issues occurring during the sending process.

Logging

trogon.dumpLogsToFile()

Writes the cached log messages to the file configured in outputLogsPath.

If outputLogsPath is not set, an error is thrown.

trogon.printLogsCache()

Outputs the current cache of log messages to the console for inspection.

FAQ

Does Trogon validate that the HTML/CSS will be understood by all email clients?

As of the current version, no, although it'd make a handy feature to have. Make sure to test your email in a variety of email clients, and consult Can I email to check support individual tags/styles.

Can I use it from CLI?

As of the current version, no, it'd make for another handy feature to have.

Do I have to use the generated EML file?

No, if you don't need the actual EML file, you can just get the current mail options from trogon.state and compose a MIME object with Nodemailer's internal methods, like so:

import MailComposer from "nodemailer/lib/mail-composer";
import Trogon from "trogon";

const state = await new Trogon({})
    .loadHtmlFromFile({ inputHtmlPath: "./body.html" })
    .composeMail().state;
const generatedMailOptions = state.mail;
const mailInMimeFormat = await new MailComposer(generatedMailOptions).compile().build();

Do I have to use await when chaining methods await new Trogon()?

Strictly speaking, you only need it if you're planning on chaining async methods. Having said that, the current implementation of Trogon constructor, wraps the instance inside a promise resolving proxy; so I would recommend always using await when calling methods and chaining methods since the proxy may render all of logic async, making some values undefined. If Trogon gets an update with a fully synchronous class, then you could use new TrogonSync() in sync code, but this is not implemented as of the current version. It's okay, though, to synchronously create a new instance like const trogon = new Trogon();

Aren't JS classes out of fashion anyhow? Why is Trogon a class and not a namespace with functions?

My personal take is that procedural code and FP are almost always better than classes and OOP, but method chaining in JS is just too satisfying to me.

Licence

This project is licensed under the Mozilla Public License Version 2.0 - see the LICENCE.md file for details.