Skip to content

About

A Lettermint component for Convex.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Lettermint Convex Component

This component integrates the Lettermint transactional email service with Convex.

Legal Notice

This project is an independent, unofficial Convex component for Lettermint. It is not affiliated with, endorsed by, sponsored by, or maintained by Lettermint.

Lettermint and related names, logos, product names, and trademarks are the property of Lettermint or their respective owners. All rights are reserved by their respective owners. Any use of Lettermint marks in this project is for descriptive and referential purposes only.

Features:

  • Queueing: Send many emails quickly; the component processes them durably.
  • Batching: Sends queued emails to Lettermint's /send/batch endpoint.
  • Durable execution: Uses Convex workpools to retry temporary failures.
  • Idempotency: Uses Convex email IDs as Lettermint Idempotency-Key values.
  • Rate limiting: Limits calls to the Lettermint Sending API.
  • Webhooks: Verifies Lettermint HMAC signatures and tracks message status.

See example for a demo of how to incorporate this component into your application.

Installation

npm install convex-lettermint

Get Started

Create a Lettermint project and create a Project API

Add the component to your Convex app via convex/convex.config.ts:

import { defineApp } from "convex/server";
import lettermint from "convex-lettermint/convex.config.js";

const app = defineApp();
app.use(lettermint);

export default app;

Use it from a Convex function:

import { components } from "./_generated/api";
import { Lettermint } from "convex-lettermint";
import { internalMutation } from "./_generated/server";

export const lettermint = new Lettermint(components.lettermint, {});

export const sendTestEmail = internalMutation({
  handler: async (ctx) => {
    await lettermint.sendEmail(ctx, {
      from: "Me <test@mydomain.com>",
      to: "ok@testing.lettermint.co",
      subject: "Hi there",
      html: "This is a test email",
    });
  },
});

testMode is enabled by default. In test mode, recipients must use the Lettermint test domain @testing.lettermint.co. Examples include:

  • ok@testing.lettermint.co
  • softbounce@testing.lettermint.co
  • hardbounce@testing.lettermint.co
  • spamcomplaint@testing.lettermint.co
  • dsn@testing.lettermint.co

Set testMode: false in LettermintOptions before sending to real recipients.

Webhooks

The component can receive Lettermint delivery, bounce, complaint, open, and click events.

Mount an HTTP endpoint in convex/http.ts:

import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { lettermint } from "./sendEmails";

const http = httpRouter();

http.route({
  path: "/lettermint-webhook",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    return await lettermint.handleLettermintEventWebhook(ctx, req);
  }),
});

export default http;

Create a webhook in the Lettermint dashboard for that URL and subscribe to the message events you need. Copy the webhook secret and set it to LETTERMINT_WEBHOOK_SECRET in your Convex deployment environment.

Lettermint signs webhooks with X-Lettermint-Signature. The component verifies the HMAC-SHA256 signature and rejects invalid or stale webhook requests.

Event Handler

Register a mutation to run when email events arrive:

import { components, internal } from "./_generated/api";
import { internalMutation } from "./_generated/server";
import { Lettermint, vOnEmailEventArgs } from "convex-lettermint";

export const lettermint = new Lettermint(components.lettermint, {
  onEmailEvent: internal.example.handleEmailEvent,
});

export const handleEmailEvent = internalMutation({
  args: vOnEmailEventArgs,
  handler: async (ctx, args) => {
    // args provides { id: EmailId; event: EmailEvent; }
    if (args.event.event === "message.delivered") {
      // Handle delivery.
    }
  },
});

Supported outbound Lettermint events include message.created, message.sent, message.delivered, message.hard_bounced, message.soft_bounced, message.spam_complaint, message.failed, message.suppressed, message.policy_rejected, message.unsubscribed, message.opened, and message.clicked. webhook.test is acknowledged and ignored.

Options

The LettermintOptions constructor argument customizes component behavior:

  • projectToken: Provide the Lettermint Project API token instead of reading LETTERMINT_PROJECT_TOKEN.
  • webhookSecret: Provide the webhook secret instead of reading LETTERMINT_WEBHOOK_SECRET.
  • testMode: Only allow delivery to Lettermint test addresses. Defaults to true.
  • initialBackoffMs: Initial retry backoff for failed API calls. Defaults to 30 seconds.
  • retryAttempts: Maximum retry attempts for failed API calls. Defaults to 5.
  • onEmailEvent: Mutation callback for Lettermint webhook events.

Sending Parameters

sendEmail supports the batch-safe Lettermint fields:

  • from
  • to
  • cc
  • bcc
  • subject
  • html
  • text
  • replyTo
  • headers
  • route
  • tag
  • metadata
  • settings.trackOpens
  • settings.trackClicks

Example:

await lettermint.sendEmail(ctx, {
  from: "Notifications <notifications@mydomain.com>",
  to: "ok@testing.lettermint.co",
  subject: "Welcome!",
  html: "<p>Welcome to our app.</p>",
  route: "transactional",
  tag: "welcome-email",
  metadata: {
    user_id: "user_123",
  },
  settings: {
    trackOpens: false,
    trackClicks: true,
  },
});

Templates are not supported by the Lettermint Sending API schema used by this component. Generate dynamic HTML yourself or use React Email before calling sendEmail.

Tracking And Status

sendEmail returns a branded EmailId.

Use it to check status or cancel queued emails:

const emailId = await lettermint.sendEmail(ctx, {
  from: "Me <test@mydomain.com>",
  to: "ok@testing.lettermint.co",
  subject: "Hi",
  text: "Hello",
});

const emailStatus = await lettermint.status(ctx, emailId);
if (emailStatus) {
  console.log(emailStatus.status);
  console.log(emailStatus.bounced);
  console.log(emailStatus.failed);
  console.log(emailStatus.complained);
  console.log(emailStatus.opened);
  console.log(emailStatus.clicked);
  console.log(emailStatus.errorMessage);
}

await lettermint.cancelEmail(ctx, emailId);

If an email has already been sent to Lettermint, it cannot be cancelled.

Data Retention

This component retains finalized emails and delivery events. You can clear old data with cleanupOldEmails and cleanupAbandonedEmails, either from the Convex dashboard or a cron job:

import { cronJobs } from "convex/server";
import { components, internal } from "./_generated/api.js";
import { internalMutation } from "./_generated/server.js";

const crons = cronJobs();
crons.interval(
  "Remove old emails from the Lettermint component",
  { hours: 1 },
  internal.crons.cleanupLettermint,
);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
export const cleanupLettermint = internalMutation({
  args: {},
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(0, components.lettermint.lib.cleanupOldEmails, {
      olderThan: ONE_WEEK_MS,
    });
    await ctx.scheduler.runAfter(
      0,
      components.lettermint.lib.cleanupAbandonedEmails,
      { olderThan: 4 * ONE_WEEK_MS },
    );
  },
});

export default crons;

React Email

Use React Email to generate HTML in a Convex Node action, then pass the generated HTML to sendEmail.

"use node";
import { action } from "./_generated/server";
import { render, pretty } from "@react-email/render";
import { Button, Html } from "@react-email/components";
import { components } from "./_generated/api";
import { Lettermint } from "convex-lettermint";

export const lettermint = new Lettermint(components.lettermint, {
  testMode: false,
});

export const sendEmail = action({
  args: {},
  handler: async (ctx) => {
    const html = await pretty(
      await render(
        <Html>
          <Button href="https://example.com">Click me</Button>
        </Html>,
      ),
    );

    await lettermint.sendEmail(ctx, {
      from: "Me <test@mydomain.com>",
      to: "recipient@example.com",
      subject: "Hi there",
      html,
    });
  },
});

Sending Manually And Attachments

Attachments are intentionally not part of the durable queue API. Use sendEmailManually when you need Lettermint features that should be sent immediately, including attachments or inline images.

import { components } from "./_generated/api";
import { internalAction } from "./_generated/server";
import { Lettermint } from "convex-lettermint";

export const lettermint = new Lettermint(components.lettermint, {});

export const sendManualEmail = internalAction({
  args: {},
  handler: async (ctx) => {
    const from = "Acme <onboarding@example.com>";
    const to = ["ok@testing.lettermint.co"];
    const subject = "hello world";
    const html = "<p>it works!</p>";

    await lettermint.sendEmailManually(
      ctx,
      { from, to, subject },
      async (emailId) => {
        const response = await fetch("https://api.lettermint.co/v1/send", {
          method: "POST",
          headers: {
            "x-lettermint-token": process.env.LETTERMINT_PROJECT_TOKEN!,
            "Content-Type": "application/json",
            "Idempotency-Key": emailId,
          },
          body: JSON.stringify({
            from,
            to,
            subject,
            html,
            attachments: [
              {
                filename: "invoice.pdf",
                content: "base64-encoded-content",
              },
            ],
          }),
        });
        if (!response.ok) {
          throw new Error(`[Email] Failed to send: ${await response.text()}`);
        }
        const data = (await response.json()) as { message_id: string };
        return data.message_id;
      },
    );
  },
});

About

A Lettermint component for Convex.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages