EmailGetting StartedSending Mail

Sending Mail

Learn different ways to send emails with @visulima/email

Last updated:

Sending Mail

This guide covers the different ways to send emails with @visulima/email.

Using the Message Builder

The most common way to send emails is using the fluent message builder API:

import { createMail, MailMessage } from "@visulima/email";
import { resendProvider } from "@visulima/email/providers/resend";

const mail = createMail(resendProvider({ apiKey: "re_xxx" }));

const message = new MailMessage().to("user@example.com").from("sender@example.com").subject("Hello").html("<h1>Hello World</h1>").text("Hello World");

const result = await mail.send(message);

Chaining Methods

The message builder supports method chaining for a fluent API:

const message = new MailMessage()
    .to("user@example.com")
    .cc("cc@example.com")
    .bcc("bcc@example.com")
    .from("sender@example.com")
    .replyTo("reply@example.com")
    .subject("Hello")
    .html("<h1>Hello World</h1>")
    .text("Hello World");

const result = await mail.send(message);

Multiple Recipients

You can send to multiple recipients:

const message = new MailMessage().to(["user1@example.com", "user2@example.com"]).from("sender@example.com").subject("Hello").html("<h1>Hello World</h1>");

const result = await mail.send(message);

Or use the EmailAddress format:

const message = new MailMessage()
    .to([
        { email: "user1@example.com", name: "User 1" },
        { email: "user2@example.com", name: "User 2" },
    ])
    .from({ email: "sender@example.com", name: "Sender" })
    .subject("Hello")
    .html("<h1>Hello World</h1>");

const result = await mail.send(message);

Direct Email Sending

You can also send emails directly using EmailOptions:

import type { EmailOptions } from "@visulima/email";

const mail = createMail(resendProvider({ apiKey: "re_xxx" }));

const emailOptions: EmailOptions = {
    from: { email: "sender@example.com" },
    to: { email: "user@example.com" },
    subject: "Hello",
    html: "<h1>Hello World</h1>",
    text: "Hello World",
};

const result = await mail.send(emailOptions);

Creating Draft Emails

You can create draft emails in EML (RFC 822) format without sending them. This is useful for previewing, saving for later, or testing email content. The draft() method returns the email as an EML string with an X-Unsent: 1 header automatically added.

import { createMail, MailMessage } from "@visulima/email";
import { resendProvider } from "@visulima/email/providers/resend";
import { writeFile } from "fs/promises";

const mail = createMail(resendProvider({ apiKey: "re_xxx" }));

// Create a draft from MailMessage
const message = new MailMessage().to("user@example.com").from("sender@example.com").subject("Hello").html("<h1>Hello World</h1>");

const eml = await mail.draft(message);

// Save draft to file
await writeFile("draft.eml", eml);

// Or preview the draft
console.log("Draft EML:", eml);

Using Drafts with EmailOptions

You can also create drafts from EmailOptions:

const eml = await mail.draft({
    to: "user@example.com",
    from: "sender@example.com",
    subject: "Hello",
    html: "<h1>Hello World</h1>",
});

// Save draft to database or file system
await saveDraftToFile("draft.eml", eml);

Drafts with Global Configuration

Drafts automatically apply global configuration (default from, reply-to, headers):

const mail = createMail(resendProvider({ apiKey: "re_xxx" }))
    .setFrom({ email: "noreply@example.com" })
    .setHeaders({ "X-App-Name": "MyApp" });

const message = new MailMessage().to("user@example.com").subject("Hello").html("<h1>Hello</h1>");

// Draft will include the global from address and headers
const eml = await mail.draft(message);
// The EML will include "X-App-Name": "MyApp" and "X-Unsent": "1" headers

Serving Drafts as Downloads

You can serve drafts as downloadable EML files (similar to Symfony):

import { createMail, MailMessage } from "@visulima/email";
import { resendProvider } from "@visulima/email/providers/resend";

const mail = createMail(resendProvider({ apiKey: "re_xxx" }));

// In your HTTP handler (e.g., Express, Fastify, etc.)
app.get("/draft/:id", async (req, res) => {
    const message = new MailMessage().to("user@example.com").from("sender@example.com").subject("Hello").html("<h1>Hello World</h1>");

    const eml = await mail.draft(message);

    res.setHeader("Content-Type", "message/rfc822");
    res.setHeader("Content-Disposition", 'attachment; filename="draft.eml"');
    res.send(eml);
});

Important Notes

  • Drafts are created in EML (RFC 822) format with the X-Unsent: 1 header automatically added
  • The draft() method returns a string (EML format) that can be saved to a file or database
  • EML files can be opened by most email clients (Outlook, Thunderbird, Apple Mail, etc.)
  • The EML format is useful for archiving, previewing, or sharing draft emails

Handling Results

All sending methods return a Result<EmailResult> object:

const message = new MailMessage().to("user@example.com").from("sender@example.com").subject("Hello").html("<h1>Hello</h1>");
const result = await mail.send(message);

if (result.success) {
    console.log("Email sent:", result.data?.messageId);
    console.log("Provider:", result.data?.provider);
    console.log("Timestamp:", result.data?.timestamp);
} else {
    console.error("Failed to send email:", result.error);
}

Error Handling

Always check the result and handle errors appropriately:

try {
    const message = new MailMessage().to("user@example.com").from("sender@example.com").subject("Hello").html("<h1>Hello</h1>");
    const result = await mail.send(message);

    if (!result.success) {
        // Handle provider-specific errors
        if (result.error instanceof Error) {
            console.error("Error:", result.error.message);
        }
        // Retry logic, fallback provider, etc.
    }
} catch (error) {
    // Handle unexpected errors
    console.error("Unexpected error:", error);
}

Validating Email Addresses

Before sending emails, you can validate email addresses to ensure they're valid and not from disposable email services.

Basic Email Validation

import { validateEmail } from "@visulima/email/validation/validate-email";

const email = "user@example.com";

if (!validateEmail(email)) {
    throw new Error("Invalid email address");
}

// Proceed with sending
const message = new MailMessage().to(email).from("sender@example.com").subject("Hello").html("<h1>Hello</h1>");
await mail.send(message);

Disposable Email Detection

To prevent sending emails to disposable/temporary email addresses, use the disposable email detection utility:

Use it before sending:

import { isDisposableEmail } from "@visulima/email/validation/disposable-email-domains";
import { createMail, MailMessage } from "@visulima/email";
import { resendProvider } from "@visulima/email/providers/resend";

const mail = createMail(resendProvider({ apiKey: "re_xxx" }));

const recipientEmail = "user@mailinator.com";

// Check if email is disposable
if (isDisposableEmail(recipientEmail)) {
    throw new Error("Disposable email addresses are not allowed");
}

// Send email if validation passes
const message = new MailMessage().to(recipientEmail).from("sender@example.com").subject("Welcome").html("<h1>Welcome!</h1>");
const result = await mail.send(message);

Validating Multiple Recipients

When sending to multiple recipients, validate all addresses:

import { isDisposableEmail } from "@visulima/email/validation/disposable-email-domains";
import { validateEmail } from "@visulima/email/validation/validate-email";
import { MailMessage } from "@visulima/email";

const recipients = ["user1@example.com", "user2@mailinator.com", "user3@example.com"];

// Filter out invalid and disposable emails
const validRecipients = recipients.filter((email) => {
    return validateEmail(email) && !isDisposableEmail(email);
});

if (validRecipients.length === 0) {
    throw new Error("No valid recipients");
}

// Send to valid recipients only
const message = new MailMessage().to(validRecipients).from("sender@example.com").subject("Hello").html("<h1>Hello</h1>");
await mail.send(message);

Custom Disposable Domains

You can also add custom disposable domains to check:

import { isDisposableEmail } from "@visulima/email/validation/disposable-email-domains";

const customDisposableDomains = new Set(["my-disposable.com", "test-temp.com"]);

if (isDisposableEmail("user@my-disposable.com", customDisposableDomains)) {
    throw new Error("Disposable email addresses are not allowed");
}

Role Account Detection

Detect if an email address is a role account (non-personal email addresses like noreply@, support@, admin@, etc.):

import { isRoleAccount } from "@visulima/email/validation/role-accounts";

if (isRoleAccount("noreply@example.com")) {
    console.log("This is a role account (non-personal)");
}

// With custom role prefixes
const customPrefixes = new Set(["custom-role", "my-role"]);
if (isRoleAccount("custom-role@example.com", customPrefixes)) {
    console.log("Custom role account detected");
}

MX Record Checking

Verify that a domain has valid MX (Mail Exchange) records before sending:

import { checkMxRecords } from "@visulima/email/validation/check-mx-records";
import { InMemoryCache } from "@visulima/email/utils/cache";

const domain = "example.com";

// Without cache
const mxResult = await checkMxRecords(domain);

// With cache (recommended for better performance)
const cache = new InMemoryCache();
const mxResultCached = await checkMxRecords(domain, {
    cache,
    ttl: 5 * 60 * 1000, // Cache for 5 minutes
});

if (!mxResultCached.valid) {
    throw new Error(`Domain ${domain} has no valid MX records`);
}

console.log("MX records found:", mxResultCached.records);

SMTP Verification

Verify if an email address exists by connecting to the mail server:

import { verifySmtp } from "@visulima/email/validation/verify-smtp";
import { InMemoryCache } from "@visulima/email/utils/cache";
import type { MxCheckResult, SmtpVerificationResult } from "@visulima/email/validation/check-mx-records";

// Create separate caches for MX records and SMTP results
const mxCache = new InMemoryCache<MxCheckResult>();
const smtpCache = new InMemoryCache<SmtpVerificationResult>();

const result = await verifySmtp("user@example.com", {
    timeout: 5000,
    fromEmail: "test@example.com",
    port: 25,
    cache: mxCache, // Optional: cache MX records
    smtpCache, // Optional: cache SMTP verification results
    ttl: 5 * 60 * 1000, // Cache for 5 minutes
});

if (result.valid) {
    console.log("Email address exists");
} else {
    console.error("Verification failed:", result.error);
}

Note: Many mail servers block SMTP verification to prevent email harvesting. This method may not work for all domains and should be used with caution.

Comprehensive Email Verification

Use the comprehensive verification function to check multiple aspects of an email address:

import { verifyEmail } from "@visulima/email/validation/verify-email";
import { InMemoryCache } from "@visulima/email/utils/cache";
import type { MxCheckResult, SmtpVerificationResult } from "@visulima/email/validation/check-mx-records";

// Create separate caches for MX records and SMTP results
const mxCache = new InMemoryCache<MxCheckResult>();
const smtpCache = new InMemoryCache<SmtpVerificationResult>();

const result = await verifyEmail("user@example.com", {
    checkDisposable: true,
    checkRoleAccount: true,
    checkMx: true,
    checkSmtp: false, // Optional: many servers block SMTP verification
    cache: mxCache, // Optional: cache MX records
    smtpCache, // Optional: cache SMTP verification results
});

if (result.valid) {
    console.log("Email passed all checks");
} else {
    console.error("Validation errors:", result.errors);
    console.warn("Warnings:", result.warnings);
}

// Access individual check results
if (result.disposable) {
    console.warn("Email is from a disposable service");
}

if (result.roleAccount) {
    console.warn("Email appears to be a role account");
}

if (result.mxValid === false) {
    console.error("Domain has no valid MX records");
}

Complete Validation Example

Here's a complete example combining all validation checks:

import { createMail, MailMessage } from "@visulima/email";
import { resendProvider } from "@visulima/email/providers/resend";
import { InMemoryCache } from "@visulima/email/utils/cache";
import type { MxCheckResult, SmtpVerificationResult } from "@visulima/email/validation/check-mx-records";
import { verifyEmail } from "@visulima/email/validation/verify-email";

const mail = createMail(resendProvider({ apiKey: "re_xxx" }));
// Reuse cache instances across requests
const mxCache = new InMemoryCache<MxCheckResult>();
const smtpCache = new InMemoryCache<SmtpVerificationResult>();

const validateAndSend = async (recipientEmail: string) => {
    // Comprehensive verification with caching
    const verification = await verifyEmail(recipientEmail, {
        checkDisposable: true,
        checkRoleAccount: true,
        checkMx: true,
        checkSmtp: false,
        cache: mxCache, // Cache MX records
        smtpCache, // Cache SMTP verification results
    });

    if (!verification.valid) {
        const errors = verification.errors.join(", ");
        throw new Error(`Email validation failed: ${errors}`);
    }

    if (verification.warnings.length > 0) {
        console.warn("Email warnings:", verification.warnings);
    }

    // Send email
    const message = new MailMessage().to(recipientEmail).from("sender@example.com").subject("Welcome").html("<h1>Welcome!</h1>");
    const result = await mail.send(message);

    if (!result.success) {
        throw result.error;
    }

    return result.data;
};

// Use the validation function
try {
    const emailResult = await validateAndSend("user@example.com");
    console.log("Email sent:", emailResult?.messageId);
} catch (error) {
    console.error("Failed to send email:", error);
}

Normalizing Email Aliases

Some email providers support aliases that point to the same mailbox. Normalize emails to their canonical form to prevent duplicate accounts:

import { normalizeEmailAliases } from "@visulima/email/utils/normalize-email-aliases";

// Gmail: removes dots and plus aliases
const normalized = normalizeEmailAliases("example+test@gmail.com");
// Returns: "example@gmail.com"

// Use before storing in database or checking for duplicates
const userEmail = normalizeEmailAliases("user.name+tag@gmail.com");
// Store "username@gmail.com" instead of "user.name+tag@gmail.com"

Supported Providers:

  • Gmail: Removes dots (.) and plus aliases (+)
  • Yahoo, Outlook, Hotmail, Live, MSN, iCloud, ProtonMail, Zoho, FastMail, Mail.com, GMX: Removes plus aliases (+) only, dots are preserved

Next Steps

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues