Message trace
Finding out what happened to a message: the queue view for mail in flight, the durable trace for mail that has left, and what each step means.
“Where is the message I sent an hour ago?” and “did the mail from our supplier arrive on Tuesday?” are the two questions the message trace answers. It has two halves:
| Half | Answers | Kept |
|---|---|---|
| The queue view | Where a message is now, while it is still being delivered. | While the message is in flight. |
| The trace | Everything that happened to a message, from acceptance to its final outcome. | 30 days, or as configured. |
Neither ever holds what a message says: only its envelope, the hosts involved, and the replies they gave.
The examples use the vsx shell function from the Quick start.
Every message has a queue id
When the server accepts a message it replies with a 26-character queue id, such as 01K5B7Z3QX9W4M2N8R6T0V1YAC:
- A sending app or server sees it in the reply
250 2.0.0 queued as 01K5B7Z3QX9W4M2N8R6T0V1YAC. - It is in the message’s
Receivedheader:by mail.example.com with ESMTPS id 01K5B7Z3QX9W4M2N8R6T0V1YAC. - It is on every log line about the message.
Ids are not case-sensitive, and O, I and L are read as 0, 1 and 1.
Finding messages by address and day
vsx admin get trace tenant=1 address=alex@example.com day=2026-09-14
vsx admin get trace tenant=1 address=alex@example.com from=2026-09-08 to=2026-09-14
| Parameter | Required | Meaning |
|---|---|---|
address | Yes | An envelope sender or recipient, exactly as it was given. Case does not matter. |
day | One day, or from and to | The day the message arrived, as YYYY-MM-DD, in UTC. |
from, to | Together, instead of day | The first and last of a run of days, a month at most. The newest day comes first. |
cursor | For the next page | The cursor the last answer over the same days ended with. |
tenant | For the operator | The tenant whose trace to search. |
A message is found under its tenant: for mail received from outside, the tenant of the first recipient’s domain; for mail submitted by a person, that person’s tenant.
{
"address": "alex@example.com",
"day": "2026-09-14",
"items": [
{
"id": "01K5B7Z3QX9W4M2N8R6T0V1YAC",
"receivedAt": 1789387200000,
"recipients": ["alex@example.com"],
"refused": null,
"sender": "sam@example.net",
"size": 48213,
"tenant": 1
}
]
}
refused is null for a message the server took, and the reply and the reason for one it refused at the door.
At most 200 messages are returned at a time. When there are more, the answer says more and ends with a cursor; pass it back as cursor=, with the same days, for the next 200. From the command line, trace search ends with the command for the next page, with --after.
Finding messages by what happened to them
“Which of yesterday’s messages to outlook.com bounced?” and “what went to quarantine this week?” are answered by searching on the outcome rather than on an address:
vsx admin trace search --outcome bounced --day 2026-10-01
vsx admin trace search outlook.com --outcome bounced --from 2026-10-01 --to 2026-10-07
vsx admin get trace tenant=1 outcome=quarantined day=2026-10-01
outcome | Found for a recipient whose message was |
|---|---|
delivered | Put in their mailbox as it came, or accepted by the next server. |
junked | Put in their Junk folder. |
quarantined | Held in their quarantine, including a copy taken back into it after delivery. |
deferred | Not delivered yet; it will be tried again. |
bounced | Refused for good by the other server, or given up on after five days. The sender was told. |
refused | Refused at the door and never accepted. |
held | Held for review before it left, because the account that sent it looked taken over. |
removed | Taken out of the queue by an administrator, or out of the mailbox it was delivered to. |
Each recipient is found once, under what happened last. A message deferred and then delivered is found as delivered. A copy released from quarantine is found as delivered again, and one taken back after delivery as quarantined or removed.
With an outcome, the address is optional. Given one, it narrows the rows to messages with that address on either side; given a domain, such as outlook.com, to messages with any address at it. The days work as they do for an address search: day, or from and to for up to a month.
The answer has a row for each message and recipient:
{
"outcome": "bounced",
"address": "outlook.com",
"from": "2026-10-07",
"to": "2026-10-01",
"items": [
{
"id": "01JZ8YQ4XN2K6R1V0M3W5T7B9E",
"receivedAt": 1790812800000,
"recipient": "x@outlook.com",
"outcome": "bounced",
"recipients": ["x@outlook.com"],
"refused": null,
"sender": "ada@example.com",
"size": 18342,
"tenant": 1
}
],
"more": false,
"cursor": null
}
Up to 200 rows are returned at a time, the newest day first. One search reads at most 5,000 entries of the index. When a search stops at that limit, or at 200 rows, the answer says more and gives a cursor. Pass it back with the same outcome and days for the rest. From the command line, trace search ends with the command for the next page.
A domain administrator sees the rows that touch their domains, and can narrow only to addresses and domains of their own.
On the console
The trace page, under Mail flow, has What happened beside the address box. Choose an outcome, and the address becomes optional; it can be an address or a domain. Each row says what happened to its recipient. More fetches the next page, and Save as CSV saves the rows shown, one line per message and recipient.
One message’s whole story
vsx admin get trace/01K5B7Z3QX9W4M2N8R6T0V1YAC tenant=1
{
"clientIp": "198.51.100.7",
"helo": "mx.example.net",
"id": "01K5B7Z3QX9W4M2N8R6T0V1YAC",
"receivedAt": 1789387200000,
"recipients": ["alex@example.com"],
"sender": "sam@example.net",
"size": 48213,
"steps": [
{
"at": 1789387200000,
"detail": "over mx",
"recipient": null,
"remote": null,
"seq": 1,
"step": "received"
},
{
"at": 1789387200000,
"detail": "Deliver; decided by thresholds, score -1.5",
"recipient": null,
"remote": null,
"seq": 2,
"step": "filtered"
},
{
"at": 1789387201000,
"detail": "filed into mailbox 1",
"recipient": "alex@example.com",
"remote": null,
"seq": 3,
"step": "delivered"
}
],
"tenant": 1
}
| Field | Meaning |
|---|---|
id | The queue id. |
sender | The envelope sender. Empty for a notification the server generated. |
recipients | The envelope recipients, as given. |
size | The message size in bytes. |
receivedAt | When the server accepted it, in Unix milliseconds. |
clientIp, helo | Where it came from. For mail sent over JMAP, clientIp is jmap. |
steps | What happened, in order. |
note | Present when the message took more than 64 steps: only the first 64 are kept. |
Steps
step | Meaning | detail |
|---|---|---|
received | The message was accepted. | How: over mx, over submission or over jmap, and for a message sent later, to go at <time>. |
refused | Refused while the sending server was still connected; the only step of a message refused at the door. | The reply the sender was given and why, and the Message-ID when the message had been read. |
filtered | The filter reached a verdict. Recorded for mail received on port 25, and again at delivery when the time budget left stages for it. | The verdict, the stage that decided and the score, and left for delivery: with the stages the time budget left; at delivery, finished at delivery (<stages>) with the finished verdict. |
delivered | Put into a local mailbox, or handed to local delivery. | filed into mailbox <ids>, or the reply 250 2.0.0 handed to local delivery. When a rule tagged the message, also its subject tagged <prefix>. |
relayed | Accepted by another server. | Its reply code, status and text. |
deferred | Not delivered this time; it will be tried again. Recorded when the reason changes. | The reply or local error, with remote naming the host. |
failed | Refused for good. | The reply or local error. |
expired | The five days ran out. | 451 4.4.7 delivery time expired; last error: … |
notified | A delivery status notification was queued to the sender. | notification queued to <address>. |
held | Held for review before it left, because the account that sent it looked taken over, or because it was scheduled to go later and was due after its account could no longer sign in. | Why it was held. |
released | Let go after review: a held message sent on, or a copy released from quarantine by an administrator or by its holder. Also a message sent later, let go at its time. | Who released it, or at the time it was scheduled for. |
removed | Taken out of the queue by an administrator, or a message sent later cancelled by its sender before its time. | Whether the sender was told, or canceled by its sender before its time. |
taken-back | A delivered copy taken back out of a mailbox: into quarantine as reported phishing, or removed by an administrator. | Where it was taken from, and why. |
A step about one recipient names it in recipient; a step about a remote host names it in remote. detail is at most 512 characters.
On an installation spread over sites, each step also has site and node: the site and the node that took it, from the node’s [placement] node. A message received in one site and sent on from another shows both. A node with no site named leaves them out.
Mail refused at the door
A message refused while the sending server was still connected on port 25 was never accepted, but it is in the trace too, because “why didn’t I get their email?” is most often answered by such a refusal. It has a queue id of its own, is found by the same address and day as the mail that arrived, and is removed with the rest by retention.
{
"id": "01K5B8C2M7Q0H3V9D4S8W1E6XN",
"receivedAt": 1789387500000,
"recipients": ["alx@example.com"],
"refused": "550 5.1.1 No such user here; unknown recipient",
"sender": "sam@example.net",
"size": 0,
"tenant": 1
}
What is kept:
| Refusal | Why, as the trace says it |
|---|---|
| A recipient nobody here has | unknown recipient |
| A recipient whose mailbox is full, or disabled | mailbox full, mailbox disabled |
| A group that does not take mail from this sender | a group that does not take mail from this sender |
| A directory that did not answer | the directory did not answer |
A DMARC reject policy | DMARC: the policy of <domain> is reject, and the message failed it |
| A sender the organisation blocks | a sender the organisation blocks |
| Greylisting | greylisted: a sender not seen before, asked to try again |
| The filter’s score | spam, scored <score>, followed by blocklisted: and the lists that named the client, if any did |
| Another filter stage | the filter's <stage> stage |
| A message too large | message too large |
| Line endings the server refuses | lines that end in a way this server refuses |
| A connection closed for guessing at addresses | the harvest limit: too many recipients refused in one session |
Temporary refusals are kept as well as permanent ones: a full mailbox is a common answer to that question. The one step, refused, names the client and the name it gave in remote. size is how much of the message had arrived, 0 when it was refused before it was sent.
Mail refused for a domain this server does not hold is not kept: it is nobody’s mail here. One network — a /24 for IPv4, a /64 for IPv6 — writes at most 100 refusals an hour one by one. After that, one entry counts the rest of that hour, and says so: 57 more refused from 198.51.100.0/24 this hour, past the first 100: counted here and not written one by one.
From the command line, trace search prints refused at the door: and the reply under each such message.
Messages still in flight
vsx admin get queue
vsx admin get queue tenant=1
The operator sees every tenant’s queue unless they name one. Each line:
| Field | Meaning |
|---|---|
id | The queue id. |
queue | transport (waiting to be routed or sent) or delivery (waiting to be put into mailboxes). |
tenant | Whose message. |
sender | The envelope sender. |
recipients | How many recipients. |
settled | How many of them are finished. |
size | Bytes. |
receivedAt | When it was accepted. |
dueAt | When it will next be worked on. |
waitingFor | Milliseconds since it was accepted. |
attempts | Delivery passes it has had. |
heldBy | The node working on it now, or null. |
requiretls | Whether the sender asked for REQUIRETLS. |
The answer ends with a note reminding you that a message that has already left is in the trace, not the queue.
One message in flight
vsx admin get queue/01K5B7Z3QX9W4M2N8R6T0V1YAC
Adds origin (clientIp and helo) and, for each recipient:
| State | Fields |
|---|---|
pending | address, and last: the last error, with at, code, status, text and remote, or null before the first attempt. |
done | address, outcome (local, relayed, failed or expired), diagnostic (as above), and senderTold: whether the sender was notified. |
A message that is not in flight — delivered, never existed, or someone else’s — gets 404 with no message with that id is in flight here; for one that has already left, ask /api/v1/trace.
Acting on mail in flight
vsx admin queue retry 01K5B7Z3QX9W4M2N8R6T0V1YAC
vsx admin queue hold 01K5B7Z3QX9W4M2N8R6T0V1YAC
vsx admin queue release 01K5B7Z3QX9W4M2N8R6T0V1YAC
vsx admin queue remove 01K5B7Z3QX9W4M2N8R6T0V1YAC --tell-sender
| Command | What it does |
|---|---|
retry | Tries the message now instead of when it is next due, for a destination that was down and is back. |
hold | Keeps it from being tried until it is released, so it can be looked at before it leaves. |
release | Lets a held message go. It is tried at once. |
remove | Takes it out of the queue. With --tell-sender, every recipient still waiting is given up on and the sender gets one notice naming them. Without it, nobody is told. |
A message a node is delivering at that moment can’t be changed; try again a moment later. Mail on its way into mailboxes here can be removed, but not returned to its sender. The trace records that an administrator removed a message, and the audit log records who acted.
Over the API these are POST /api/v1/queue/{id}/retry, …/hold and …/release, and DELETE /api/v1/queue/{id}?bounce=true to remove a message and tell its sender.
Several messages at once
Choose messages by where they are going, by who sent them, or both. Each can be an address or a domain:
vsx admin queue retry-all --to example.org
vsx admin queue hold-all --from newsletter@example.com
vsx admin queue release-all --from newsletter@example.com
vsx admin queue remove-all --from compromised@example.com --tell-sender
- A domain matches addresses at that domain, not at the names under it.
--toand--fromtogether choose the messages that match both.- At least one of them is needed, so nothing acts on the whole queue.
- Up to 1,000 messages are acted on at once. When more match, nothing is done; narrow the choice.
- A message that can’t be acted on right now is skipped, with the reason, and the rest go ahead. Each message acted on has its own line in the audit log.
Over the API these are POST /api/v1/queue/retry, /hold, /release and /remove, with to, from or ids (a list of queue ids), and "bounce": true to tell senders when removing. The answer lists the messages done and those skipped, each with why.
Taking a message back out of mailboxes
When a message that should not have been delivered got through, such as a phishing message or one sent to the wrong list, take every copy back out of the mailboxes it reached. Name it by the queue id it arrived under, or by its Message-ID:
vsx admin message purge 01K5B7Z3QX9W4M2N8R6T0V1YAC --dry-run
vsx admin message purge 01K5B7Z3QX9W4M2N8R6T0V1YAC
vsx admin message purge '<parcel-4411@phish.example>'
--dry-run shows which mailboxes hold a copy, and in which folders, and removes nothing. Without it, every copy is removed, wherever its owner has filed it since, a copy held in quarantine included. The answer names each account and the folders its copy was in, never what the message says, and each removal is its own line in the audit log.
A queue id finds the copies delivered under it. A Message-ID finds the copy each mailbox holds under that id, however it arrived.
Tenant administrators can remove a message, and domain administrators can in their own domains. Auditors can look with --dry-run. Over the API it is POST /api/v1/tenants/{tenant}/purge with queueId or messageId, and "dryRun": true to look first.
Common questions
| Question | Where to look |
|---|---|
| Did a message reach us? | trace by the recipient’s address and the day. A message with refused set was refused at the door, with the reply the sender was given. No result at all means the sending server never got as far as naming the recipient. |
| Why did it go to Junk? | The filtered step’s score, and the X-Versealx-Filter header on the message. |
| Which messages bounced yesterday? | trace search --outcome bounced --day with yesterday’s date, narrowed to a domain if you like. |
| Why hasn’t my message arrived? | queue for the sender’s messages; then queue/<id> for each recipient’s last error. |
| Did the other side accept it? | trace/<id>: a relayed step with the remote server’s reply. |
| Was the sender told it failed? | notified in the trace, or senderTold in the queue view. |
Retention
Traces are removed once they are older than the retention period: 30 days by default, [admin] trace_days in the configuration file, or the runtime setting trace.keep_days, which wins without a restart. An organisation can set its own trace.keep_days, which wins over the server’s. Expiry runs every ten minutes on nodes with the relay role, removing the oldest first a little at a time so that it never holds up mail, and the log says old message traces forgotten.
A trace is a record of who wrote to whom, so keep it only as long as you need it.
Something unclear or out of date on this page? Tell us.