email

Production-ready SMTP email library for EZ. Supports plain, HTML, attachments, CC/BCC, and multiple recipients over pure libcurl FFI.

0 downloads owners: imabd645 repository

Install

ez install email

Dependencies (1.0.0)

PackageRange
No dependencies.

Readme

email

SMTP email for EZ — pure libcurl FFI, no C++ glue.

email is an SMTP client library for the EZ programming language that lets you send plain-text, HTML, and multipart emails with attachments, directly over libcurl. There is no compiled C++ extension in this package — every call into the operating system goes through EZ's built-in FFI (os_load_lib / os_get_func / os_call), all funneled through a single file (src/ffi.ez) so the rest of the library stays pure EZ.

version: 2.0

Table of Contents


Features


Requirements

Platform note: the current FFI layer loads Windows DLLs (libcurl-4.dll, libcurl.dll, msvcrt.dll) by name. This release targets Windows; a POSIX build would need ffi.ez to load a shared object (libcurl.so) instead.

If neither libcurl DLL can be found, the library throws a clear error at import time rather than failing silently later.


Installation

Place the package where your EZ project can reach it and import the public entry point:

use "email"

This pulls in main.ez, which internally wires up src/ffi.ez, src/errors.ez, src/message.ez, src/mime.ez, and src/client.ez. You only ever need to use "email" — the src/ files are implementation details.


Quick Start

use "email"

client = connect("smtp.gmail.com", 587, "me@gmail.com", "app-password")

client.send(
    Message()
        .from("me@gmail.com")
        .to("alice@example.com")
        .subject("Hello!")
        .html("<h1>Hi Alice!</h1>")
        .attach("report.pdf")
)

Or, for a single throwaway email without holding onto a client:

use "email"

send_quick("smtp.gmail.com", 587, "me@gmail.com", "app-password",
           "me@gmail.com", "alice@example.com", "Hi", "Hello!")

Usage Guide

Connecting

connect() returns a configured SmtpClient you can reuse for multiple sends:

client = connect(server, port, username, password)
ArgumentDescription
serverSMTP hostname, e.g. "smtp.gmail.com"
port465 (implicit SSL/SMTPS), 587 (STARTTLS), or 25 (plain)
usernameLogin / email address
passwordPassword or App Password

SmtpClient picks smtps:// vs smtp:// automatically based on whether port == 465.

Building a Message

Message is a fluent builder — every setter returns self:

msg = Message()
    .from("me@gmail.com")
    .to("alice@example.com")
    .cc("bob@example.com")
    .bcc("audit@company.com")
    .subject("Hello!")
    .html("<h1>Hi Alice!</h1>")
    .attach("report.pdf")
    .priority("high")
    .header("X-Campaign", "summer25")

Only from, to, and subject are required, plus at least one of text() / html(). SmtpClient.send() calls msg.validate() first and throws EmailError if anything required is missing — before any network activity happens.

Sending

client.send(msg)

send():

  1. Validates the message.
  2. Builds the RFC 2822 MIME payload.
  3. Writes it to a uniquely named temp file.
  4. Configures and performs the curl SMTP transfer.
  5. Cleans up the temp file, curl handle, and recipient list — always.
  6. Returns true on success, or re-throws a structured exception on failure.

One-shot Sends

For scripts that only need to send a single plain-text email:

send_quick(server, port, username, password, fromAddr, toAddr, subject, body)

This is sugar for connect() + Message().from(...).to(...).subject(...).text(...) + send().

Attachments

msg.attach("report.pdf")
msg.attach("C:\\Users\\me\\Documents\\invoice.pdf")

CC, BCC, and Reply-To

msg.cc("bob@example.com")
msg.cc(["bob@example.com", "carol@example.com"])   // array form also accepted

msg.bcc("audit@company.com")
msg.replyTo("support@example.com")

Priority

msg.priority("high")    // X-Priority: 1, Importance: High
msg.priority("normal")  // X-Priority: 3 (no Importance header)
msg.priority("low")     // X-Priority: 5, Importance: Low

Custom Headers

msg.header("X-Campaign", "summer25")
msg.header("X-Mailer", "email/2.0")

Custom headers are appended after the standard headers and before the body.

TLS / SSL Configuration

SmtpClient is secure by default. These fields can be set directly on the client instance before calling send():

FieldDefaultMeaning
useSsltrueEnables CURLUSESSL_ALL — require SSL for the entire session
verifyCerttrueVerifies the server's TLS certificate and hostname
useNativeCatrueUses the Windows native certificate store (CURLSSLOPT_NATIVE_CA)
caBundlePath""Optional path to a custom ca-bundle.crt, used if verifyCert is on
minTlsCURL_SSLVERSION_TLSv1_2Minimum negotiated TLS version
verbosefalsePrints the full curl handshake to stdout for debugging
client = connect("smtp.internal.corp", 25, "svc@corp", "pass")
client.verifyCert = false   // ⚠ insecure — internal/self-signed servers only
client.send(msg)
Disabling verifyCert disables both peer and host verification. Only do this against trusted internal infrastructure — never over the public internet.

Debugging

client.verbose = true

Prints the raw curl handshake (CURLOPT_VERBOSE) to stdout, useful for diagnosing connection or auth issues.

You can also check which libcurl DLL was actually loaded:

out backend()   // "libcurl-4.dll" or "libcurl.dll"

Error Handling

All exceptions raised by this library are typed models, never raw strings:

ExceptionThrown When
EmailErrorGeneric/base failure — missing required fields, curl init failure, unmapped curl error codes
SmtpConnectionErrorlibcurl can't reach the server (DNS resolution, TCP connect, or TLS handshake failure)
SmtpAuthErrorThe server rejects the credentials (curl code 67)
AttachmentErrorAn attached file can't be read from disk

Every model implements toString() for easy logging:

try {
    client.send(msg)
} catch (e) {
    out e.toString()
    // e.g. "SmtpAuthError: Authentication failed for user 'me@gmail.com'. Check your password or App Password."
}

mapCurlError() in errors.ez is what translates raw curl integer codes into the right exception type after curl_easy_perform fails.


Architecture

main.ez is the public surface only. All real work lives under src/, with a strict one-directional dependency chain and a single point of contact with the operating system:

                 ┌───────────────┐
                 │   main.ez     │   connect() / send_quick() / backend()
                 └───────┬───────┘
                         │ use
        ┌────────────────┼────────────────┬───────────────┐
        ▼                ▼                ▼               ▼
 src/client.ez     src/message.ez    src/mime.ez     src/errors.ez
        │                                 │               ▲
        │                                 └───────use──────┘
        │
        └──────────────use────────────────► src/ffi.ez
                                                   │
                                                   ▼
                                        libcurl-4.dll / libcurl.dll
                                              msvcrt.dll

Send Sequence

  1. main.connect() constructs an SmtpClient.
  2. client.send(msg) calls msg.validate().
  3. mime.buildPayload(msg) renders the RFC 2822 string.
  4. The payload is written to a temp file (ez_email_<clock>.tmp).
  5. A curl easy handle is initialized and configured: URL, auth, envelope MAIL FROM / RCPT TO, TLS options, upload source.
  6. curl_easy_perform runs the SMTP conversation.
  7. Any non-CURLE_OK result is mapped to a structured exception via mapCurlError().
  8. Cleanup always runs — file handle closed, recipient slist freed, curl handle cleaned up, temp file deleted — regardless of success or failure.
  9. If an error occurred, it's re-thrown after cleanup; otherwise true is returned.

File-by-File Reference

FileResponsibility
main.ezPublic API: connect(), send_quick(), backend(). Imports and re-exports everything else.
src/ffi.ezLoads libcurl-4.dll/libcurl.dll and msvcrt.dll; exposes function tables (__CurlFFI, __MsvcrtFFI) and libcurl option/result constants.
src/errors.ezException models: EmailError, SmtpConnectionError, SmtpAuthError, AttachmentError; mapCurlError() translator.
src/message.ezMessage fluent builder model: recipients, subject, body, attachments, priority, custom headers, validation.
src/mime.ezbuildPayload() — turns a Message into an RFC 2822 wire payload, including base64 attachment encoding and MIME boundaries.
src/client.ezSmtpClient model — TLS configuration, curl handle lifecycle, temp-file lifecycle, error mapping on send.

Resource Safety

SmtpClient.send() follows a manual "always cleanup" pattern rather than relying on finally, because EZ does not run finally after give. Instead:

This guarantees no leaked curl handles, no leaked slist memory, and no orphaned temp files, even when a send fails partway through.


MIME Payload Format

buildPayload() supports three content strategies depending on what's set on the Message:

ScenarioContent-Type
Plain text onlytext/plain; charset=UTF-8
HTML only, no attachmentstext/html; charset=UTF-8
Any attachment presentmultipart/mixed; boundary="..." — one text/html body part, plus one application/octet-stream part per attachment

Additional details:


Design Notes & Caveats


License

MIT License

Versions

VersionSizeDownloadsPublished
1.0.0 15.1 KB 0 1 hour ago

Integrity

sha256  a39366d618657a3f572259e28badd973ceb2140c2024fd1a1c37d31a79bdff36