mailer

High-level mailer for EZ. HTML templates, connection pooling, and provider presets for Gmail, SendGrid, Mailgun, AWS SES, and Outlook.

0 downloads owners: imabd645

Install

ez install mailer

Dependencies (1.0.0)

PackageRange
email ^1.0.0

Readme

mailer

High-level mailer for EZ — provider presets, HTML templates, and connection pooling on top of email.

mailer is a convenience layer built on top of the email SMTP package. It doesn't talk to libcurl or touch FFI at all — instead it wraps email.connect() / email.Message / email.SmtpClient with three things application code actually wants: one-liner provider setup (Gmail, SendGrid, Mailgun, SES, Outlook, Yahoo), a small {{ key }} HTML template engine for transactional email, and a connection pool for bulk sending.

depends on: email (email)

Table of Contents


Relationship to email / email

mailer is not a replacement for email — it's built directly on top of it:

If you only need to send a single plain email, email alone is enough. Reach for mailer when you want provider shortcuts, file-based HTML templates, or pooled bulk sending.


Features


Installation

use "mailer"

This pulls in main.ez, which internally imports email (the email package) plus src/errors.ez, src/template.ez, src/pool.ez, and src/providers.ez. As with email, you only ever need the single top-level use — the src/ files are implementation details.

Prerequisite: mailer requires email to be installed/importable as "email", since it calls email.connect() under the hood and reuses email.Message.

Quick Start

Provider preset + template + send

use "mailer"

client = Providers.gmail("me@gmail.com", "app-pass")
html   = Template.render("emails/welcome.html", {"name": "Alice"})

client.send(
    Message()
        .from("me@gmail.com")
        .to("alice@example.com")
        .subject("Welcome!")
        .html(html)
        .attach("welcome_guide.pdf")
)

Connection pool for bulk sending

use "mailer"

pool = createPool("smtp.gmail.com", 587, "me@gmail.com", "pass", 3)
pool.sendAll(messages)

Usage Guide

Provider Presets

Providers is a static model — call its tasks directly without instantiating it. Every preset returns a ready-to-use SmtpClient (the same type email.connect() returns), so .send() works immediately.

client = Providers.gmail("me@gmail.com", "app-password")
client = Providers.gmailSSL("me@gmail.com", "app-password")   // port 465 / SMTPS
client = Providers.outlook("me@outlook.com", "password")
client = Providers.yahoo("me@yahoo.com", "app-password")
client = Providers.sendgrid("SG.xxxxxxxxxx")
client = Providers.mailgun("mg.myapp.com", "smtp-password")
client = Providers.ses("AKID...", "smtp-secret...", "us-east-1")
client = Providers.custom("mail.myserver.com", 587, "user", "pass")
PresetHostPortUsernameNotes
gmail(username, appPassword)smtp.gmail.com587your Gmail addressRequires a Google App Password, not your regular password (Google Account → Security → 2-Step Verification → App Passwords)
gmailSSL(username, appPassword)smtp.gmail.com465your Gmail addressImplicit SSL/SMTPS variant of the above
outlook(username, password)smtp-mail.outlook.com587your Outlook/Hotmail address
yahoo(username, appPassword)smtp.mail.yahoo.com587your Yahoo addressRequires a Yahoo App Password
sendgrid(apiKey)smtp.sendgrid.net587literal string "apikey"apiKey is your SendGrid key, starting with SG.
mailgun(domain, smtpPassword)smtp.mailgun.org587postmaster@<domain>smtpPassword is the Mailgun SMTP password, not the general API key
ses(smtpUser, smtpPassword, region)email-smtp.<region>.amazonaws.com587SES SMTP usernameCredentials come from the SES console's SMTP Settings, not your AWS access key
custom(server, port, username, password)anyanyanyEscape hatch for anything not covered above

All presets default to port 587 with STARTTLS except gmailSSL (465/implicit SSL). TLS behavior beyond that (cert verification, min TLS version, CA bundle, verbose logging) is still controlled through the returned SmtpClient's fields, exactly as in email:

client = Providers.custom("mail.internal.corp", 25, "svc", "pass")
client.verifyCert = false   // ⚠ internal/self-signed servers only

Templates

Template is a static model for rendering {{ key }}-style HTML templates.

html = Template.render("emails/welcome.html", {
    "name":    "Alice",
    "confirm": "https://myapp.com/confirm/abc123"
})

Syntax rules:

Three entry points:

TaskPurpose
Template.render(templatePath, vars)Reads an HTML file from disk and injects vars. Throws TemplateError if the file is missing, unreadable, or empty.
Template.renderString(html, vars)Same substitution logic, but for an HTML string already in memory (no file I/O).
Template.inject(html, vars)The underlying substitution routine both of the above call.

Example template file (emails/welcome.html):

<h1>Welcome, {{ name }}!</h1>
<p>Click <a href="{{ confirm_url }}">here</a> to confirm your account.</p>

sendTemplate Helper

For the common case of "render a template, attach it as the HTML body, send it," main.ez exposes a one-call helper:

sendTemplate(client, msg, templatePath, vars)
mailer.sendTemplate(
    Providers.gmail("me@gmail.com", "app-pass"),
    Message().from("me@gmail.com").to("alice@example.com").subject("Welcome!"),
    "emails/welcome.html",
    {"name": "Alice", "confirm_url": "https://app.com/confirm/abc"}
)

Internally this is just:

html = Template.render(templatePath, vars)
msg.html(html)
give client.send(msg)

— i.e. it mutates the passed-in msg by setting its HTML body, then sends it through the passed-in client. Build the Message with from/to/subject (and anything else) first; sendTemplate fills in the body.

Connection Pool

createPool() (or ConnectionPool directly) pre-allocates a fixed number of SmtpClient instances so that repeated sends in a loop don't pay curl-init cost per message.

pool = createPool("smtp.gmail.com", 587, "me@gmail.com", "pass", maxSize = 5)
ArgumentDescription
server, port, username, passwordSame as email.connect()
maxSizeNumber of SmtpClients to pre-allocate (default 5)

What the pool does and does not do: it does not pre-open TCP sockets — libcurl manages actual connection reuse internally (CURLOPT_MAXCONNECTS). What it does manage is object-level checkout/checkin: it keeps N SmtpClient objects allocated, marks a slot "in use" while a send is in flight, and returns it to the pool when the send finishes (whether it succeeded or threw).

Single send, borrowing from the pool:

pool.send(msg)

Batch send:

sent = pool.sendAll(messages)

Inspecting pool state:

pool.status()
// { "total": 5, "in_use": 2, "available": 3 }

Error Handling

mailer adds three exception types on top of everything email already throws (EmailError, SmtpConnectionError, SmtpAuthError, AttachmentError — see the email README):

ExceptionThrown When
MailerErrorGeneric mailer-level failure; also used by pool.sendAll() to summarize batch failures
PoolExhaustedErrorpool.send()/pool.sendAll() is called when all maxSize connections are already checked out
TemplateErrorA template file is missing, unreadable, or empty

All three implement toString():

try {
    pool.send(msg)
} catch (e) {
    out e.toString()
    // e.g. "PoolExhaustedError: All 5 SMTP connections are in use. Try again later or increase pool size."
}

Because mailer builds directly on email, a pool.send() or client.send() call can also raise any of email's own exceptions (SmtpAuthError, SmtpConnectionError, AttachmentError, EmailError) — catch broadly if you want to handle both layers uniformly.


Architecture

                    ┌───────────────┐
                    │   main.ez     │   createPool() / sendTemplate()
                    └───────┬───────┘
                            │ use
         ┌──────────────────┼──────────────────┬───────────────┐
         ▼                  ▼                  ▼               ▼
  src/providers.ez     src/pool.ez       src/template.ez  src/errors.ez
         │                  │                    │               ▲
         │                  │                    └────use────────┤
         │                  └──────────use───────────────────────┤
         └─────────────────────────use───────────────────────────┘
                            │
                            ▼
                     "email" (email)
                  connect() / Message / SmtpClient

File-by-File Reference

FileResponsibility
main.ezPublic API: createPool(), sendTemplate(). Imports email plus all src/ modules.
src/errors.ezException models: MailerError, PoolExhaustedError, TemplateError.
src/template.ezTemplate static model — render() (from file), renderString() (from memory), inject() (substitution engine) for {{ key }} placeholders.
src/pool.ezConnectionPool model — pre-allocates SmtpClients, checkout/checkin, send(), sendAll(), status().
src/providers.ezProviders static model — one-liner presets for Gmail, Gmail SSL, Outlook, Yahoo, SendGrid, Mailgun, SES, and custom SMTP.

Thread Safety

ConnectionPool instances are not safe to share across threads. Per the source comments: EZ's spawn() model gives each thread its own VM, so a pool (and the SmtpClient objects inside it) created on one thread is not visible to or usable by another. Create one pool per thread, not one shared pool.


Design Notes & Caveats

Versions

VersionSizeDownloadsPublished
1.0.0 10.2 KB 0 1 hour ago

Integrity

sha256  58034c9003bb8272e18563bb533cf9f73cbb0af769b6b9662b4c67e2fc674206