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 streamBy 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.