Library / SDK
eleith/emailjs avatar
eleith/emailjs

emailjs: an SMTP client, and what version 5 broke for CommonJS

html emails and attachments to any smtp server with nodejs

2,209 stars231 forksTypeScriptMIT

At a glance

What is it?
emailjs is a MIT-licensed Node SMTP client with first-party TypeScript declarations, structured attachments including inline images, and a synchronous validity check, tested against a real in-process SMTP server. Version 5 ships ESM-only, so CommonJS consumers cannot require it, and there are no GitHub releases to track versions against.
Who is it for?
Adopt emailjs when mail must leave through an SMTP relay you control, and call message.checkValidity() before every sendAsync so malformed messages fail in your process rather than at a receiving server.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 46 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

An SMTP client, and nothing above it

emailjs is a Node library for sending rich HTML emails, attachments from files, streams or strings, and plain text messages to any SMTP server. The README's list of expectations is short and specific: SSL and TLS support, the popular authentication methods, asynchronous sending, rich content, flexible attachments, UTF-8 in headers and body, built-in TypeScript declarations, and greylisting awareness.

What is absent from that list is as informative as what is on it. There is no template engine, no HTML builder, no bounce or complaint webhook handling, no scheduling, and no HTTP provider client. If you send through SendGrid or Postmark you would be talking to their REST API, and this library does not do that.

The scope is a direct SMTP conversation with a server, which is a legitimate and sometimes necessary choice. Some organisations require mail to leave through an on-premises relay, and some want the same code path in development and production rather than a sandbox HTTP endpoint. For those cases, an SMTP client is exactly the right amount of abstraction.

Two named authors, eleith and zackschuster, and a MIT licence. The repository is not archived and the last push was on 2026-08-26, with `package.json` at version 5.0.3. There are no GitHub releases, so the npm version is the only version marker to track.

Installing and sending the first message

Installation is one command and the only hard requirement is an SMTP server to talk to.

bash
npm install emailjs

The README notes one practical requirement beyond that: if your email service, Gmail being the example it uses, has two-step verification enabled, you need an application-specific password rather than your account password.

The first example is the shape of every send. Construct a client, call `sendAsync`, close the connection.

javascript
import { SMTPClient } from 'emailjs';

const client = new SMTPClient({
    user: 'your-username',
    password: 'your-password',
    host: 'smtp.your-email.com',
    ssl: true,
});

async function sendMyEmail() {
    try {
        const message = await client.sendAsync({
            text: 'Hello from emailjs! This is a test message.',
            from: 'You <[email protected]>',
            to: 'Someone <[email protected]>',
            subject: 'Exciting News from emailjs!',
        });
        console.log('Email sent successfully:', message);
    } catch (err) {
        console.error('Failed to send email:', err);
    } finally {
        client.smtp.close();
    }
}

Two things in that snippet are easy to get wrong. The `finally` block closes the connection, and without it you leak a socket per send. And the return value is the server's response details rather than a boolean, which is where you read the queue identifier if the server issued one.

There is a callback form too, `send(message, (err, details) => ...)`, for code that predates promises. Both take either a `Message` instance or a plain headers object.

ssl, tls and the port that decides which one you get

The client options are where the transport is decided, and the defaults are documented inline.

javascript
const options = {
    user: 'your-username',
    password: 'your-password',
    host: 'smtp.your-email.com',
    port: 587,
    ssl: true,
    tls: true,
    timeout: 5000,
    domain: 'your-domain.com',
    authentication: ['PLAIN', 'LOGIN'],
    logger: console,
};

Three defaults worth memorising. `host` falls back to `localhost`. `port` follows the security mode: 25 unencrypted, 465 for immediate SSL, 587 for STARTTLS. And `domain` defaults to `os.hostname()`, which is the value greeted to the server in `EHLO` and which some providers reject when it is a container hostname rather than your domain.

The distinction between `ssl` and `tls` is not cosmetic. `ssl: true` requests an immediate SSL connection, which is the implicit TLS on port 465. `tls: true` initiates STARTTLS, which is the upgrade on port 587 where the connection starts in the clear and becomes encrypted after the handshake. Both accept a boolean or an object, and the object form takes the underlying module's options.

`timeout` is 5000 milliseconds by default, which is the maximum wait for an SMTP response. On a slow relay or a server that queues, that is the first value to raise.

`authentication` is an ordered preference list, `PLAIN` and `LOGIN` being the common pair. The README also lists `CRAM-MD5` and `XOAUTH2`, which covers Gmail's OAuth path and most modern providers.

Attachments from a path, a stream or a string

The HTML example is where this library earns its place, because attachments are declared as structured entries rather than as a separate MIME builder call.

javascript
const message = new Message({
    from: 'You <[email protected]>',
    to: 'Someone <[email protected]>',
    subject: 'Your Awesome HTML Email!',
    attachment: [
        {
            data: htmlContent,
            alternative: true,
            contentType: 'text/html',
        },
        {
            path: 'path/to/your/document.pdf',
            type: 'application/pdf',
            name: 'document.pdf',
        },
    ],
});

The first entry is the HTML body rather than a file, marked with `alternative: true` and `contentType: 'text/html'`. That is how a multipart message gets both a text and an HTML representation, and the library assembles the boundary.

The second is a file read from disk at send time, with an explicit MIME `type` and a `name` the recipient sees.

`Message#attach(options)` adds the same entries one at a time and can be called repeatedly. The option set covers `path` for a file on disk, `data` for raw string or buffer content, `stream` for a readable stream, `type` and `name`, `alternative` for inline alternatives, `inline` for images referenced in the HTML, and `headers` for anything else the MIME part needs.

That last combination is how the embedded image works. An entry with `headers: { 'Content-ID': '<my-image>' }` and `inline: true` is referenced from the body as `<img src="cid:my-image">`, so the image travels inside the message instead of being fetched from a URL. That matters for clients that block remote images by default.

checkValidity before you send

There is one validation hook and it is worth calling on every send in anything but a script.

javascript
const { isValid, validationError } = message.checkValidity();
if (!isValid) {
    console.error('Message is invalid:', validationError);
}

`Message#checkValidity()` runs synchronously and returns a destructurable pair, so the failure path gives you a reason rather than a boolean.

That matters because the alternative is discovering a malformed message as a send failure, or worse as a message that was accepted and bounced. `Message` is documented as RFC2822-compliant, and the constructor's own header set gives the minimum: `from` is required, and at least one of `to`, `cc` or `bcc` must be present, with `subject`, `text` and `attachment` all optional.

The asynchronous sending mentioned in the feature list is a separate concern. Emails are queued and sent in the background, so `sendAsync` returning is not the same instant as the message leaving. For a library whose selling point includes deliverability, knowing that is the difference between a correct integration and one that reports success for a message that is still sitting in a queue.

Greylisting awareness is a retry, not a deliverability strategy

The feature list claims greylisting awareness, with a link to the greylisting whitepaper, and describes it as automatically handling greylisting to improve deliverability.

Worth being precise about what that can be. Greylisting is a spam filter behaviour where a server receiving from an unfamiliar sender rejects the first message with a temporary failure and accepts anything resent from the same address within a window. The mitigation is a retry after a delay, and that is what a library can do. What no library can do is fix a reputation, a reverse DNS record, an SPF or DKIM configuration, or content that a recipient reports as spam.

So treat it as one narrow reliability feature. The option that governs it is `timeout`, because a greylisting rejection arrives as a 4xx response and the client needs to be around long enough to hear it, and then whatever retry interval the library applies.

The rest of the deliverability surface is yours. There is no bounce handling, no complaint webhook, no unsubscribe list management and no domain authentication helpers in what the README describes. If deliverability is your actual problem rather than your integration problem, this library will not move the needle, and you should be looking at your sending reputation instead.

Version 5 is ESM-only, and the resolutions field names a package you do not depend on

Two packaging details decide whether you can adopt this at all.

The package is `"type": "module"`, with a single `exports` entry pointing at `./dist/index.js` and types at `./dist/index.d.ts`. There is no `require` condition and no CommonJS build in the manifest. A CommonJS application cannot `require('emailjs')` on version 5; it has to be ESM or dynamic-imported. `engines` requires Node 18 or newer, and TypeScript 5.0 or newer is an optional peer dependency for the declaration files.

The second detail is odd enough to be worth reading twice. The manifest contains a `resolutions` entry pinning `nodemailer` to 6.7.4, and nodemailer appears in neither `dependencies` nor `devDependencies`.

json
"resolutions": {
    "nodemailer": "6.7.4"
}

That field is also a yarn concept, while the package ships with `package-lock.json` and npm scripts. A resolutions entry for a package that is not in the tree is inert on a normal install and can only do something if a transitive dependency pulls nodemailer in, which would explain the pin as a transitive override and its absence from the declared list. Either way, if you were expecting the library to depend on nodemailer, it does not.

Everything else in the manifest is conventional: `build` runs `tsc`, `lint` runs `eslint src/*.ts`, `test` and `test:coverage` run vitest, and `prepublishOnly` builds before publishing.

Testing against smtp-server, and Mailpit by hand

The test story is the strongest part of the repository, and it is in the dev dependencies rather than the README.

`smtp-server` at 3.17.1 is a Node SMTP server implementation, and `mailparser` at 3.9.1 parses what comes back. Together they mean the test suite can start a real SMTP server in the same process, speak the protocol to it, and assert on the resulting message. That catches the class of bug a mocked mailer hides, which is MIME structure: boundaries, encodings, header folding, the order of parts.

Type declarations for both are pinned, `@types/smtp-server` and `@types/mailparser`, which is what lets this stay TypeScript end to end under vitest 4 with v8 coverage.

For manual work there is a `docker-compose.yml` running `axllent/mailpit`.

yaml
services:
  mailpit:
    image: axllent/mailpit
    ports:
      - '8025:8025'
      - '1025:1025'
    environment:
      MP_SMTP_AUTH: 'user:password123 dev:secret'
      MP_SMTP_TLS_CERT: 'sans:localhost'
      MP_SMTP_TLS_KEY: 'sans:localhost'

That gives you a web interface on port 8025 to read what your client actually produced and an SMTP listener on 1025 to send it to, with self-signed certificates generated on the fly and two local accounts. The credentials in that file are local test values for a container bound to your machine, not shared secrets.

The two commented-out environment lines are the useful part. `MP_SMTP_REQUIRE_STARTTLS` simulates a strict server, and setting `MP_SMTP_AUTH_ALLOW_INSECURE` to false forces your client to negotiate TLS before sending credentials, which is exactly the regression to test against.

Editorial conclusion

Adopt emailjs when mail must leave through an SMTP relay you control, and call message.checkValidity() before every sendAsync so malformed messages fail in your process rather than at a receiving server. Do not adopt it if you send through an HTTP provider API, since there is no transport other than SMTP, and check that your application can load an ESM-only package before upgrading past 4.x. Verify first that checkValidity returns isValid true for your real messages, and set domain explicitly rather than letting it default to os.hostname(), which some relays reject.

Frequently asked questions

How do I install emailjs and send my first email?

Run npm install emailjs, construct an SMTPClient with your user, password, host and ssl or tls flag, and call sendAsync with from, to, subject and text or attachment. Close the connection with client.smtp.close() when you are done.

Can I use emailjs in a CommonJS project?

Not on version 5. The package manifest declares type module with a single ESM export and no require condition, so CommonJS consumers need to be ESM or use a dynamic import. The manifest also requires Node 18 or newer.

How do I embed an image in an HTML email?

Add an attachment entry with headers set to Content-ID and inline set to true, then reference it in the body as img src with the cid: scheme. The image then travels inside the message instead of being fetched from a URL.

Does emailjs depend on nodemailer?

The manifest does not list nodemailer in dependencies or devDependencies, though it does contain a resolutions entry pinning nodemailer to 6.7.4, which is a yarn field and can only act as a transitive override.

What is greylisting awareness in emailjs?

The feature list says the library automatically handles greylisting to improve deliverability. Greylisting is a temporary rejection from an unfamiliar sender, so what the client can do is survive and retry it, not repair sending reputation or domain authentication.

How does emailjs get tested?

The dev dependencies include smtp-server and mailparser, so the vitest suite starts a real SMTP server in process and parses the message that comes back. A docker-compose.yml also provides axllent/mailpit for manual checks on ports 8025 and 1025.

Official sources

  1. eleith/emailjs on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/eleith-emailjs.svg)](https://hysenlabs.com/projects/eleith-emailjs)