Postbox docs

Receiving

What happens to inbound mail before it reaches you, and how to react to it.

Any mailbox on a domain with receivingEnabled and a published MX record accepts inbound mail. There is nothing else to turn on.

The inbound pipeline

sender ──► mx.postboxapp.cloud (SMTP)
              │
              ├─ size / attachment checks
              ├─ greylisting
              ├─ spam scoring
              ├─ inbound policy + sender rules
              │
              ▼
        parse MIME, thread, store body + attachments
              │
              ▼
        message.received  ──► webhooks + SSE stream

By the time you see message.received, the message is parsed, threaded, filed into a folder, and its attachments are in object storage.

Reacting to it

Two ways, same payload, and you can use both:

  • Webhooks: durable and retried. Use this when missing a message is unacceptable, which is most server-side work.
  • Events (SSE): live and resumable. Use this to drive a UI.
// webhook handler
if (event.type === "message.received") {
  const { mailboxId, messageId, from, subject } = event.data;
}

Then fetch the full message when you need the body:

import { Postbox } from "@postbox/sdk";

const pb = new Postbox({
  apiKey: process.env.PB_KEY!,
  projectId: process.env.PROJECT_ID!,
});

const message = await pb.messages.get(messageId);
import os
from postbox import Postbox

pb = Postbox(api_key=os.environ["PB_KEY"], project_id=os.environ["PROJECT_ID"])

message = pb.messages.get(message_id)
using Postbox;

var pb = new PostboxClient(
    new PostboxOptions
    {
        ApiKey = Environment.GetEnvironmentVariable("PB_KEY")!,
        ProjectId = Environment.GetEnvironmentVariable("PROJECT_ID")!,
    }
);

var message = await pb.Messages.GetAsync(messageId);
import cloud.postboxapp.*;
import cloud.postboxapp.gen.*;
import java.util.*;

Postbox pb = new Postbox(PostboxOptions.builder().apiKey(System.getenv("PB_KEY")).projectId(System.getenv("PROJECT_ID")).build());

var message = pb.messages.get(messageId);
import postbox "github.com/postboxapp/postbox-go"

pb, err := postbox.NewWithKey(os.Getenv("PB_KEY"))
if err != nil {
    log.Fatal(err)
}
ctx := context.Background()

message, err := pb.Messages.Get(ctx, messageID)
if err != nil {
    log.Fatal(err)
}
curl https://api.postboxapp.cloud/v1/messages/$MESSAGE_ID \
  -H "Authorization: Bearer $PB_KEY"

Inbound policy

Policy decides what is accepted and where it lands. Your org's inbound policy sets the spam threshold, the action on spam, and SPF/DKIM rejection; sender rules add per-sender allow and block overrides on top.

const policy = await pb.inboundPolicy.get();
policy = pb.inbound_policy.get()
var policy = await pb.InboundPolicy.GetAsync();
var policy = pb.inboundPolicy.get();
policy, err := pb.InboundPolicy.Get(ctx)
if err != nil {
    log.Fatal(err)
}
curl https://api.postboxapp.cloud/v1/inbound-policy/inbound \
  -H "Authorization: Bearer $PB_KEY"

See Inbound policy & sender rules for the full field set and how a message flows through rules, auth checks, and scoring.

Greylisting

First-time senders are temporarily rejected with a 4xx and asked to retry. Legitimate mail servers retry within minutes; most bulk spam software does not.

The cost is that the first message from a new sender is delayed, typically by a few minutes. The delay applies once per sender and does not repeat, including when testing with a fresh address.

Spam scoring

Inbound mail is scored and then filed or rejected against the project's spamThreshold.

If scoring is unavailable, the behaviour is configured rather than incidental. The deployment either fails open (accept the message, the default) or closed (reject with a 451, which asks the sender to retry). Fail-closed prioritises never accepting unscored mail; fail-open prioritises never losing mail during an outage.

Attachments

Attachments are parsed out and stored in object storage, keyed by attachment id rather than by filename. Fetch a message to get its attachment metadata.

Filenames from inbound mail are attacker-controlled and are never used as storage paths. If you write inbound attachments to your own disk, do the same thing: a sender picks that filename, not you.