Mailwurf

Messages

What Mailwurf reads out of an email, and how to pick the link or code a test needs.

Every accepted email becomes one message. The wait and list calls return it already parsed.

type Message = {
  id: string;
  inboxId: string;
  from: string;
  to: string;
  subject: string;
  text: string;
  html: string;
  links: string[];
  otp: string;
  receivedAt: Date;
};

Over HTTP the same fields are snake_case: inbox_id, received_at.

Fields

FieldContains
fromThe sending server's return address. With Resend or SES this is often a bounce address, not the visible sender. Mail sent from another Mailwurf inbox has that inbox's address.
toThe inbox address.
subjectDecoded subject line.
textPlain-text part. For HTML-only mail, a text version of the HTML.
htmlHTML part, or "".
linksURLs found in the mail, in order of appearance, without duplicates.
otpThe first one-time code found, or "".
receivedAtWhen Mailwurf accepted the mail.

Attachments are not exposed.

Assert on subject, text, or html when the visible sender matters. Do not assert on from.

links collects every href in the HTML first, then every http:// or https:// URL in HTML and text. That includes logo links, image URLs, mailto: links, and unsubscribe footers. Trailing ), ,, ., and ; are removed from each URL.

Pick the link by what it points to, not by position:

const mail = await inbox.waitFor({ subject: "Reset your password" });

const reset = mail.links.find((url) => url.includes("/reset-password"));
if (!reset) throw new Error(`no reset link in: ${mail.links.join(", ")}`);

await page.goto(reset.replaceAll("&", "&"));

HTML entities in href values stay as written. A link with query parameters can contain &; replace it before opening the URL.

Tracking redirects (for example from click tracking in your mail provider) are not followed. Turn link tracking off for test environments, or match on the redirect domain.

One-time codes

otp is filled in this order:

  1. A code within 64 characters after one of the words code, otp, pin, token, or passcode (any case). A code is 4–8 digits, or 4–8 uppercase letters and digits with at least one of each. Words in between are skipped, so the code can sit on the next line. If another number comes first (a time, a date), that label is skipped and the next one is tried. A year such as 2026 right after a label is only used when no other step finds a code. otp, pin, token, and passcode must be whole words.
  2. Otherwise, the first standalone 6-digit number.
  3. Otherwise, the first line that holds only a 6-character code of uppercase letters and digits.
  4. Otherwise "".

URLs are ignored, so a ?code= parameter in a link is never read as the code. Each step checks the text part, then the visible HTML, before the next step runs.

Your verification code is 482193.  → otp "482193"
Use PIN: 7731 to sign in.           → otp "7731"
Your code: A7K2QX                   → otp "A7K2QX"
Code: 482 193                       → otp ""

Codes with spaces, dashes, or lowercase letters are not detected. Read them from text yourself:

const code = mail.text.match(/\b([A-Z0-9]{3}-[A-Z0-9]{3})\b/)?.[1];

code also matches at the end of a longer word, so "Bestätigungscode" works but "barcode" can pick up an unrelated code. Inside a word ("encoded") it is not a label. If a template mixes numbers, assert on text directly.

On this page