Skip to content

Reference

JSON output

The machine-readable result documents of every command: fields, outcomes, error codes, and compatibility rules.

Add --json to send, resend (including --list), queue list|show|delete|stats, validate, auth status|refresh|logout, version, or export-templates to get a machine-readable result. The documents are the same for every sending provider (Mailgun, SMTP, Gmail); provider-specific details are noted below. This page documents schema version 1.

Rules

  • Each invocation prints exactly one JSON document, on a single line, to stdout. It is not JSON Lines; a batch (resend) is one document with an array and a summary.
  • Nothing else is printed to stdout. Diagnostics, warnings (for example, about configuration file permissions or expired deferred messages), and progress details go to stderr in human-readable form.
  • Exit codes are the same as without --json, and exit_code in the document always equals the process exit code.
  • Failures are reported as JSON too, including usage and configuration errors, whenever the command line contains a --json (or --json=true) flag, even if it cannot be parsed otherwise. --json counts only where Mailfire reads it as a flag: not as the value of another flag (--message --json), not after a -- terminator (a -- that is the value of a flag, as in --config --, is no terminator), and for send, validate, and version not after the first positional argument. An unknown flag is assumed to take no value. For an unknown subcommand, whose flags are unknown, any --json before -- counts.
  • Help is always text: --help with --json prints the usual help and exits with 0.
  • --json cannot be combined with send --preview, which writes HTML instead of a result (usage error, exit 1).
  • Timestamps are RFC 3339 strings in UTC with second precision, for example "2026-10-17T09:15:00Z".
  • Optional fields are omitted when they do not apply; they are never null. Arrays are present whenever the command produced data, also when it failed (for example, a partial resend), and may be empty.
  • Credentials are never included: no Mailgun API key, SMTP password, or Gmail token. Message bodies and attachments are never included, except the stored request in queue show --raw --json. Recipients, subjects, and error messages are included as they are; error messages from the providers are redacted like everywhere else in Mailfire.
  • Strings are valid UTF-8 with standard JSON escaping; <, >, and & are not escaped.

Common fields

Every document has these fields:

FieldTypeDescription
schema_versionnumber1
commandstringsend, resend, queue list, queue show, queue delete, queue stats, validate, auth status, auth refresh, auth logout, auth (usage errors), version, export-templates; queue when the queue subcommand is missing or unknown; mailfire for an unknown subcommand
outcomestringResult of the command; values per command below. failed whenever the command could not do its job
exit_codenumberThe process exit code
errorobjectPresent if and only if exit_code is not 0

The error object:

FieldTypeDescription
codestringStable identifier, see error codes
messagestringHuman-readable explanation; wording may change between releases

If a command fails before it produces data (usage error, configuration error, unreadable defer_dir), the document contains only the common fields:

{"schema_version":1,"command":"send","outcome":"failed","exit_code":1,"error":{"code":"usage_error","message":"missing required flag: --to"}}

Error codes

CodeExit codeMeaning
usage_error1Invalid invocation or input: unknown flag or subcommand, missing or conflicting flags, invalid address, unreadable body, attachment, or footer
unknown_id1A deferred message ID given to resend, queue show, or queue delete does not exist
export_failed1export-templates could not write the templates, for example because they already exist
config_error2Configuration file or variable missing or invalid, unusable logfile; validate: domain unknown to Mailgun; Gmail without a usable OAuth client; resend: a message’s provider is not configured or its account changed (the message is kept untouched, exit 5)
dedup_error2 (send), 5 (resend)Notification deduplication state cannot be used; nothing was sent
auth_failed3Credentials rejected: Mailgun API key (401/403), SMTP authentication (535), Gmail not logged in or authorization revoked (run mailfire auth login --provider gmail)
temporary_failure4 or 5Retryable failure (network error, timeout, HTTP 429 or 5xx, SMTP 4xx). Exit 4 when the message was deferred (or for validate), 5 when it was not (--no-defer, defer = false, --test)
rejected5The provider rejected the message permanently: Mailgun or Gmail 4xx, SMTP 5xx (for example a rejected recipient; nothing was delivered to anyone)
send_failed5Other non-retryable sending failure, for example an interrupted request, missing STARTTLS, or an untrusted TLS certificate
defer_failed5Sending failed and the message could not be stored in defer_dir; it is lost
queue_error5defer_dir or a stored message cannot be read, locked, or deleted

send

FieldTypeDescription
providerstringmailgun, smtp, or gmail; omitted when the configuration could not be loaded
outcomestringsent, accepted (test mode: Mailgun validated the request, nothing was delivered), skipped (duplicate notification), deferred, or failed
test_modebooleantrue with --test
message_idstringWith sent and accepted: Mailgun’s message ID, Gmail’s message ID, or for SMTP the Message-ID header Mailfire generated (SMTP servers return no reliable ID)
deferred_idstringID of the stored message; with deferred. Use it with mailfire resend <id> or mailfire queue show <id>
skipobjectWith skipped, see below

skip fields: reason (cooldown, queued, or in_doubt), dedup_key, queued_id (with queued: the deferred message), since (with cooldown: when the earlier notification was sent; with in_doubt: when the interrupted attempt started), and eligible_at (with cooldown and in_doubt).

sent means the provider accepted the message. For SMTP that is the submission server’s 250 reply, not final delivery to the recipient’s mailbox. --test is a usage error (usage_error, exit 1) with SMTP and Gmail, which have no validation-only mode.

{"schema_version":1,"command":"send","outcome":"sent","exit_code":0,"test_mode":false,"message_id":"<20261017091500.1@mg.example.com>"}
{"schema_version":1,"command":"send","outcome":"deferred","exit_code":4,"error":{"code":"temporary_failure","message":"mailgun: send message: status 503: Service Unavailable"},"test_mode":false,"deferred_id":"20261017T091500.123456789-9f3ab2c17e4d88aa"}
{"schema_version":1,"command":"send","outcome":"skipped","exit_code":0,"test_mode":false,"skip":{"reason":"cooldown","dedup_key":"disk-full-db1","since":"2026-10-17T09:00:00Z","eligible_at":"2026-10-17T10:00:00Z"}}

resend

FieldTypeDescription
outcomestringok (no failures), partial (some messages sent, others failed), or failed
messagesarrayOne record per processed message, in processing order
summaryobjectsent, skipped, failed, and expired counts
errorobjectWith a non-zero exit code: the error of the most severe failure (ranked as the exit code: 3, 5, 4)

Message record:

FieldTypeDescription
idstringDeferred message ID
providerstringProvider the message was deferred through and is resent through
outcomestringsent, skipped, failed, or expired (removed for exceeding max_age; this can include messages that were not selected)
reasonstringWith skipped: not_eligible (cooldown or backoff), permanently_failed (retry with --force <id>), or gone (removed by another process). With failed when nothing was attempted: provider_unavailable (the message’s provider is not configured) or account_mismatch (a different SMTP account or Google account is configured now). Such messages are kept with unchanged retry state and are retried normally once the configuration is restored
message_idstringWith sent
next_retrystringWhen the message becomes eligible; with not_eligible and with failures of code temporary_failure
warningstringWith sent, when the message could not be removed from defer_dir and might be sent again
errorobjectWith failed
{"schema_version":1,"command":"resend","outcome":"partial","exit_code":4,"error":{"code":"temporary_failure","message":"mailgun: send message: status 503: failure"},"messages":[{"id":"20261015T090000.000000001-aa","outcome":"sent","message_id":"<ok@mg.example.com>"},{"id":"20261015T100000.000000001-bb","outcome":"failed","next_retry":"2026-10-17T09:25:00Z","error":{"code":"temporary_failure","message":"mailgun: send message: status 503: failure"}},{"id":"20261017T091400.000000001-cc","outcome":"skipped","reason":"not_eligible","next_retry":"2026-10-17T09:19:00Z"}],"summary":{"sent":1,"skipped":1,"failed":1,"expired":0}}

With nothing to resend, messages is empty and outcome is ok.

resend –list and queue list

resend --list --json prints the same document as queue list --json, with command set to resend.

FieldTypeDescription
messagesarrayDeferred messages, oldest first; may be empty

Message record (also used by queue show):

FieldTypeDescription
idstringDeferred message ID
providerstringmailgun (also for messages stored by versions before provider support), smtp, or gmail
deferred_atstringWhen the message was deferred
toarray of stringsRecipients
subjectstringSubject
sizenumberSize of the stored request in bytes
attemptsnumberFailed resend attempts
last_attemptstringLatest failed resend attempt; omitted if never resent
last_errorstringLatest error; omitted if none recorded
statusstringpending, failed (permanently rejected, excluded from automatic retries), or expired (older than max_age)
next_retrystringWith pending: when the message becomes eligible; a time in the past means it is eligible now
{"schema_version":1,"command":"queue list","outcome":"ok","exit_code":0,"messages":[{"id":"20261015T090000.000000001-aa","provider":"mailgun","deferred_at":"2026-10-15T09:00:00Z","to":["ops@example.com"],"subject":"Disk full","size":812,"attempts":2,"last_attempt":"2026-10-17T09:00:00Z","last_error":"mailgun: send message: status 503: failure","status":"pending","next_retry":"2026-10-17T09:20:00Z"}]}

queue show

FieldTypeDescription
messageobjectThe message record described above
rawobjectOnly with --raw: content_type (Mailgun: the multipart Content-Type; SMTP and Gmail: message/rfc822) and body_base64 (the stored request, base64-encoded because it may be binary)

--raw is the only way to get message content in JSON. The stored request contains the message and its attachments, never credentials. For SMTP the stored message has no Bcc header; the envelope recipients, Bcc included, are kept in the queue metadata only.

queue delete

FieldTypeDescription
outcomestringok, partial (some messages deleted before an error), or failed
deletedarray of stringsIDs deleted
not_deletedarray of stringsRequested IDs that were not deleted
unknown_idsarray of stringsWith unknown_id: the unknown IDs. Nothing was deleted

queue stats

stats object: messages, failed, expired, eligible, total_size (bytes), and attempts (failed resend attempts, all messages); optionally oldest_id, oldest_deferred_at, last_attempt, and next_retry (earliest retry of a pending message).

validate

FieldTypeDescription
checksarrayChecks performed, in order; a failed check is the last one
domainobjectMailgun only, when all checks passed: name and state as reported by Mailgun

Check record: name, ok (boolean), and message. Names: config_file, mandatory_keys, then the provider check: api_key_and_domain (Mailgun), smtp_connection (SMTP: connection, TLS negotiation, and authentication; no message is sent and no recipient is probed), or gmail_authorization (Gmail: the stored authorization is usable, refreshed if nearly expired; nothing is sent).

{"schema_version":1,"command":"validate","outcome":"failed","exit_code":3,"error":{"code":"auth_failed","message":"mailgun rejected the API key: mailgun: validate domain: status 401: Forbidden"},"checks":[{"name":"config_file","ok":true,"message":"config file found and parses as valid TOML: /home/me/.config/mailfire/mailfire.conf"},{"name":"mandatory_keys","ok":true,"message":"mandatory keys present and well-formed (region \"eu\" from config file, domain \"example.com\" from config file, api key from config file)"},{"name":"api_key_and_domain","ok":false,"message":"mailgun rejected the API key: mailgun: validate domain: status 401: Forbidden"}]}

auth status, auth refresh, auth logout

FieldTypeDescription
providerstringgmail
credential_filestringThe credential file used (never its content)
logged_inbooleanstatus only
expires_atstringstatus and refresh: when the current access token expires (it is renewed automatically)

auth login is interactive and has no JSON output.

{"schema_version":1,"command":"auth refresh","outcome":"ok","exit_code":0,"provider":"gmail","credential_file":"/home/me/.config/mailfire/gmail-credentials.json","expires_at":"2026-10-17T10:15:00Z"}

version

Fields: version, commit, and build_date.

{"schema_version":1,"command":"version","outcome":"ok","exit_code":0,"version":"1.4.0","commit":"a1b2c3d","build_date":"2026-10-15T08:00:00Z"}

export-templates

Field: directory, the target folder with a leading ~ expanded.

Examples with jq

# Send and capture the message ID; fail the script on any error.
id=$(mailfire --json --to ops@example.com --subject "Backup done" --message "ok" | jq -er .message_id)

# Distinguish outcomes in a monitoring script.
result=$(mailfire --json --dedup-key disk-db1 --to ops@example.com --subject "Disk full" --file report.txt)
case $(jq -r .outcome <<<"$result") in
  sent | skipped) ;;
  deferred) echo "queued as $(jq -r .deferred_id <<<"$result")" ;;
  *) echo "failed: $(jq -r .error.code <<<"$result")" >&2 ;;
esac

# Resend and report failed messages.
mailfire resend --json | jq -r '.messages[] | select(.outcome == "failed") | "\(.id) \(.error.code)"'

# Count deferred messages and permanently rejected ones.
mailfire queue stats --json | jq '.stats | {messages, failed}'

# IDs of permanently rejected messages, for review.
mailfire queue list --json | jq -r '.messages[] | select(.status == "failed") | .id'

# Alert on authentication problems only.
mailfire validate --json | jq -e '.error.code != "auth_failed"' >/dev/null || echo "credentials rejected"

Compatibility

schema_version is 1. Within a schema version:

  • Existing fields keep their name, type, and meaning, and error codes keep their meaning.
  • New fields, new error codes, new outcome, status, or reason values, and new commands may be added. Ignore unknown fields and handle unknown values, for example by treating an unknown error code by its exit code.
  • Field order is not significant, and message texts may change.

Removing or renaming a field, changing its type or meaning, or changing the meaning of an error code requires a new schema_version. Check schema_version if your script depends on exact semantics.

Edit this page on GitHub Last updated