Skip to content

Message injection

Injection takes a raw message over HTTPS and stores it as if it had arrived on port 25. A test uses it to build and debug its own to filters and pinned patterns against a message it controls, to replay a message captured from real mail, or to try Waridex from a network that blocks port 25, as many home and cloud networks do.

Injection proves nothing about how the app under test sends its mail, so every injected message says so wherever it is shown. Nothing is relayed and nothing leaves Waridex: the message is stored in the inbox you name and nowhere else.

Terminal window
curl -sS --fail-with-body "$API/v1/inboxes/$INBOX_ID/messages" \
-H "Authorization: Bearer $WARIDEX_API_KEY" \
-H "Content-Type: message/rfc822" \
--data-binary @message.eml

POST /v1/inboxes/{id}/messages needs the email:inject scope, which a new key does not start with: add it on the key’s workspace in the dashboard. The body is the message exactly as it would follow SMTP’s DATA: RFC 5322 headers and body, sent as Content-Type: message/rfc822. Nothing in it is rewritten, and GET /v1/messages/{id}/raw returns the bytes you sent. Any other content type is 415 request.invalid; a request that names none is read as a message.

{
"messages": [
{ "id": "0199a3c4-8e2f-7b90-a1c4-5d6e7f809a1b", "envelopeTo": "user1@signup-k3f9x2.waridex.email", "created": true }
]
}

The answer is 201 Created when at least one row was made and 200 OK when every recipient already had the message, both with one row per recipient in the order they were taken. Location names the first message’s GET /v1/messages/{id}.

A Message-ID header is required: without one the call is 400 message.message_id_required. It is what makes a retry safe. The same Message-ID to the same recipient of the same inbox never makes a second row, whether the first arrived over SMTP or by injection, so a test that retries after a lost response stores nothing twice.

A message saved from a mail client carries one. A message written by hand needs a line such as Message-ID: <sample-1@example.com>.

  • to, repeatable, names them. Each must be an address of the inbox: any local part under its domain, compared ignoring case.
  • Without to, the recipients are the addresses of the message’s To, Cc and Bcc headers that belong to the inbox, in that order and each once; when the headers name none, the inbox’s default address.
  • A to naming any other address — another inbox’s, a foreign domain, the bare receiving domain — is 400 message.recipient_invalid naming it. The headers are read the other way about: an address of theirs that is not the inbox’s is passed over, so a message captured from real mail keeps its original To and still lands. Either way, more than 20 recipients is 400 message.too_many_recipients.

As on port 25, each recipient gets its own message, all of them sharing one stored copy, and envelopeTo is the address as given, so wait with to= finds each. The envelope sender is the first address of the From header, or empty where the header has none, as SMTP records a null sender.

What injection changes, and what it does not

Section titled “What injection changes, and what it does not”

Everything after the message is admitted is the pipeline mail from port 25 goes through: extraction when it arrives, the monthly quota counted once per message and workspace, pending wait calls completed, the workspace’s statistics, and retention.

Two things differ, and both are visible in the message:

FieldInjectedReceived over SMTP
sourceApiSmtp
spfResult, dkimResultleft outthe sender’s results, for diagnosis

SPF and DKIM are not evaluated for an injected message: there is no connecting client to check, and a signature on a replayed message would describe its first delivery. Every message the API returns carries source, so a suite that mixes injected and received mail can tell them apart.

The first four are the limits port 25 applies, and the key’s 600 calls a minute apply besides. The last two are the route’s own: an injection is an HTTP request the API must hold open while it reads the message, which mail arriving on port 25 is not.

LimitBeyond it
10 MB a message413 message.too_large, checked against Content-Length before the body is read and while a body without one is read
the message must parse400 message.unparsable
the plan’s monthly quotaon Free, 403 quota.exceeded with tier, quota and resetAt, and nothing is stored; on Team and Business the message is accepted and counted
300 recipients a minute for the workspace, the minute mail on port 25 shares429 rate_limit.exceeded with Retry-After until the window ends. Each recipient takes a place, as it would at RCPT TO, so one call to twenty addresses spends twenty
eight injections at once in the API, two of them for one workspacethe call waits for its turn; one that waits more than ten seconds is 429 rate_limit.exceeded with Retry-After
the body must keep up 64 KB a second after its first five secondsbelow that the request ends with 408 request.invalid, so a slow upload cannot hold a turn for long

A call answered anything but 201 or 200 takes no place in the workspace’s minute: one the rate limit refuses takes none, and one that fails after it gives its places back, so an outage cannot spend the minute. The inbox itself is checked first, so another workspace’s inbox, a deleted one and an expired one are 404 inbox.not_found.

The Email page uploads one .eml file at a time into an inbox, through the same route: “Upload .eml” in the inbox’s header, beside Refresh and Manage. It is for the file a test run leaves behind — a message whose code was extracted wrongly, or a sender’s new template — and the viewer then shows it with its codes, candidates and links, so “use this code as the pattern” can pin a pattern from it before the app under test is wired up.

The Email page's upload dialog with a file chosen: the inbox it will go to, the file's name and size, and a note that the file's own To, Cc and Bcc addresses under the inbox are its recipients, that the message counts towards the monthly quota and that it is marked Injected.

There is no recipient field: the file’s own addresses decide, as above. The dialog refuses a file over 10 MB before sending anything, and a message the API refuses leaves the file chosen so it can be sent again. Everything in this page holds for the upload as for a call from a test, the Message-ID included. Workspace roles holding email:inject — Member and Admin, not Viewer — see the action, and the API refuses the others regardless.

ProblemWhen
400 message.message_id_requiredthe message has no Message-ID
400 message.recipient_invalida recipient is not an address of the inbox
400 message.too_many_recipientsmore than 20 recipients
400 message.unparsablethe message could not be parsed
403 quota.exceededa Free workspace is over its monthly quota
403 scope.missingthe key or the member does not hold email:inject
404 inbox.not_foundthe inbox is another workspace’s, deleted or expired
413 message.too_largethe message is over 10 MB
415 request.invalidthe content type is not message/rfc822
429 rate_limit.exceededthe workspace’s inbound minute is spent, or no turn freed in ten seconds
503 store.unavailablethe message store is unreachable; nothing is stored