EmailEventsEvents

Events

Track the email lifecycle with a unified event shape, an event bus, and an event store from @visulima/email

Events

@visulima/email/events gives you a single, provider-agnostic way to observe what happens to your mail. Both internal send-lifecycle signals (queued, attempt, sent, failed) and normalized provider webhook events (delivered, opened, clicked, bounced, complained, deferred, unsubscribed) converge onto one EmailEvent shape, so you can wire a single subscriber and reconstruct a complete per-message timeline from one stream.

import { ALL_EVENTS, EventBus, MemoryEventStore } from "@visulima/email/events";
import type { EmailEvent, EmailEventType, EventStore } from "@visulima/email/events";

The EmailEvent Shape

Every event — whether emitted by the send pipeline or derived from a webhook — uses the same interface:

interface EmailEvent {
    /** A unique id for this event. */
    id: string;

    /** The kind of event. */
    type: EmailEventType;

    /** When the event occurred. */
    timestamp: Date;

    /** The message this event concerns. */
    messageId?: string;

    /** The recipient address this event concerns, when applicable. */
    recipient?: string;

    /** The provider that produced (or will produce) the message. */
    provider?: string;

    /** Arbitrary structured payload (provider response, bounce sub-type, click URL, …). */
    data?: Record<string, unknown>;
}

Event Types

type is an EmailEventType, one of:

TypeOriginDescription
queuedSend pipelineThe message was accepted into a queue.
attemptSend pipelineA delivery attempt is starting.
sentSend pipelineThe provider accepted the message for delivery.
deliveredProvider webhookThe receiving mail server accepted the message.
openedProvider webhookThe recipient opened the message.
clickedProvider webhookThe recipient clicked a tracked link.
bouncedProvider webhookThe message bounced (hard or soft).
complainedProvider webhookThe recipient marked the message as spam.
deferredProvider webhookDelivery was temporarily deferred by the receiver.
failedSend pipelineA delivery attempt failed.
unsubscribedProvider webhookThe recipient unsubscribed.

The first group (queuedfailed) is emitted by the send pipeline; the rest typically originate from provider webhooks (delivery and engagement signals).

EventBus

EventBus is a tiny synchronous pub/sub bus for EmailEvents. Subscribe to a specific EmailEventType or to ALL_EVENTS ("*") to receive everything.

import { ALL_EVENTS, EventBus } from "@visulima/email/events";

const bus = new EventBus();

// Subscribe to a single type.
const unsubscribe = bus.on("sent", (event) => {
    console.log(`Sent ${event.messageId} via ${event.provider}`);
});

// Subscribe to every event.
bus.on(ALL_EVENTS, (event) => {
    console.log(`[${event.type}]`, event.messageId);
});

// Stop listening.
unsubscribe();

Methods

MethodDescription
on(type, listener)Subscribes a listener. Returns an unsubscribe function.
once(type, listener)Subscribes a listener that is removed after its first invocation.
off(type, listener)Removes a previously-added listener.
emit(event)Dispatches an event to type-specific listeners, then to ALL_EVENTS ones.
clear(type?)Removes all listeners, or only those for a single type when type is given.

Logging Lifecycle Events

Wire an EventBus once and let every part of your app emit onto it:

import { ALL_EVENTS, EventBus } from "@visulima/email/events";
import type { EmailEvent } from "@visulima/email/events";

const bus = new EventBus();

bus.on(ALL_EVENTS, (event: EmailEvent) => {
    console.log(`[${event.timestamp.toISOString()}] ${event.type}`, {
        messageId: event.messageId,
        recipient: event.recipient,
        provider: event.provider,
    });
});

// Emit from your send pipeline.
bus.emit({
    id: "evt_1",
    type: "queued",
    timestamp: new Date(),
    messageId: "msg_42",
    recipient: "user@example.com",
});

bus.emit({
    id: "evt_2",
    type: "sent",
    timestamp: new Date(),
    messageId: "msg_42",
    recipient: "user@example.com",
    provider: "resend",
});

// Emit from a normalized webhook handler.
bus.emit({
    id: "evt_3",
    type: "delivered",
    timestamp: new Date(),
    messageId: "msg_42",
    recipient: "user@example.com",
    provider: "resend",
});

One-Time Listeners

Use once() to react to the next event of a type and then auto-remove the listener:

bus.once("bounced", (event) => {
    console.warn(`First bounce for ${event.recipient}`, event.data);
});

MemoryEventStore

MemoryEventStore is an in-memory EventStore that groups events by messageId, so you can reconstruct a complete, time-ordered timeline for any single message. Events without a messageId are kept under a shared bucket so nothing is silently dropped.

import { MemoryEventStore } from "@visulima/email/events";

const store = new MemoryEventStore();

Methods

MethodDescription
append(event)Appends an event to its message's bucket.
timeline(messageId)Returns the time-ordered events for one message (oldest first).
all()Returns every stored event, ordered by timestamp.
clear()Removes all stored events.

Reconstructing a Per-Message Timeline

Append events to the store as they arrive, then read back the full timeline for a message:

import { EventBus, MemoryEventStore } from "@visulima/email/events";

const bus = new EventBus();
const store = new MemoryEventStore();

// Persist every event the bus sees.
bus.on("*", (event) => {
    store.append(event);
});

// … events are emitted over the message's lifecycle …
bus.emit({ id: "evt_1", type: "queued", timestamp: new Date("2026-06-01T10:00:00Z"), messageId: "msg_42" });
bus.emit({ id: "evt_2", type: "sent", timestamp: new Date("2026-06-01T10:00:01Z"), messageId: "msg_42" });
bus.emit({ id: "evt_3", type: "delivered", timestamp: new Date("2026-06-01T10:00:05Z"), messageId: "msg_42" });
bus.emit({ id: "evt_4", type: "opened", timestamp: new Date("2026-06-01T10:02:00Z"), messageId: "msg_42" });

// Reconstruct the full, ordered history for one message.
for (const event of store.timeline("msg_42")) {
    console.log(event.type, "at", event.timestamp.toISOString());
}
// queued    at 2026-06-01T10:00:00.000Z
// sent      at 2026-06-01T10:00:01.000Z
// delivered at 2026-06-01T10:00:05.000Z
// opened    at 2026-06-01T10:02:00.000Z

The EventStore Interface

MemoryEventStore is the built-in implementation. To persist events durably (database, log pipeline, analytics warehouse), implement the EventStore interface yourself:

import type { EmailEvent, EventStore } from "@visulima/email/events";

class DatabaseEventStore implements EventStore {
    public async append(event: EmailEvent): Promise<void> {
        await db.emailEvents.insert(event);
    }

    public async timeline(messageId: string): Promise<EmailEvent[]> {
        return db.emailEvents.where({ messageId }).orderBy("timestamp", "asc");
    }
}

Both append and timeline may be synchronous or asynchronous, so the same interface fits an in-memory store and a remote database alike.

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