Skip to content
Norbelys
Esc
↑↓navigate↵open⌘Jpreview
On this page

Inbox and delivery feedback

Connect IMAP, inspect queue ownership and delivery reports, and handle recipient feedback without duplicate sends.

Provider acceptance, delivery to a recipient server, and inbox placement are different observations. Norbelys exposes each available piece of evidence without claiming a remote inbox location that it cannot observe.

Examples use the deployed API contract: snake_case fields and native Rust enum values. Verify the live OpenAPI when integrating another installation.

Connect a provider

For a previously provisioned Norbelys SMTP credential, call POST /v1/mailboxes using your workspace API key:

{
  "email": "smtp@example.com",
  "name": "Example shared domain",
  "provider": "Norbelys",
  "smtp": {
    "username": "smtp@example.com",
    "password": "YOUR_DOMAIN_SMTP_PASSWORD"
  },
  "imap": { "host": "YOUR_IMAP_HOST", "port": 993, "security": "Tls" }
}

This uses the operator-configured Norbelys SMTP host, STARTTLS on port 587 and the explicit optional IMAP connection. It does not create server accounts, change DNS or verify domain ownership. Call PATCH /v1/mailboxes/{id} with {"status":"Verifying"} to verify SMTP authentication; inspect inbox status separately for IMAP health. The connection initially has a 50-message daily limit. Use POST /v1/mailboxes to supply another provider’s settings. For Google or Microsoft native API connections, use transport: "Api" and the consent flow below.

Open-source clients contain public host/port defaults and ask the user for their credential. Never embed the operator’s admin key, transport key, DKIM private key or another customer’s password. Shared-domain identities share one receiving inbox and one sending allowance.

Connect Gmail or Microsoft Graph

The operator must configure a registered OAuth application and HTTPS callback for each provider. Native API support is included in the deployed code; each installation still needs provider configuration and real-account consent and delivery validation. Create the account through the same resource:

{
  "email": "you@example.com",
  "provider": "Google",
  "transport": "Api",
  "receive": true
}

Use provider: "Microsoft" for Microsoft Graph. Do not supply SMTP or IMAP credentials. The account starts in AuthorizationRequired; open the returned authorization.url. Your registered callback must bind the current workspace/mailbox and submit the returned code and state through authenticated PATCH /v1/mailboxes/{id}:

{
  "oauth": {
    "action": "Complete",
    "code": "CALLBACK_CODE",
    "state": "CALLBACK_STATE"
  }
}

The server verifies the same email identity and granted permissions before activating it. State is single-use and expires after ten minutes. Do not persist callback values in logs. To restart or change receiving permission, PATCH {"oauth":{"action":"Authorize","receive":true}}. {"oauth":{"action":"Disconnect"}} clears the stored grant and stops the connection; revoke the application in the provider’s account settings to remove its remote permission. Tokens are encrypted, refreshed by the backend and never returned to clients.

Receiving is optional. Gmail uses history IDs; Graph uses delta links and immutable message IDs. These private cursors are not IMAP UIDs. Polling uses bounded pages, durable deduplication and tenant-bound leases. An expired cursor triggers a rescan. Initial reception starts with new mail; import_history on a new watch intentionally imports older messages. Use provider folder identifiers for extra watches; do not assume an IMAP folder name maps to a Graph ID.

A large Graph message is read only up to 256 KiB. Oversized Gmail raw responses fall back to selected metadata, with the body omitted. A message marked truncated needs the original at its provider for complete attachment or delivery-report inspection. size_bytes may be a lower bound for a truncated HTTP response without Content-Length.

Watch IMAP folders

receiving in GET /v1/mailboxes/{id} returns at most eight folder summaries: folder, polled_at, next_poll_at and status_detail. Timestamps use RFC 3339 UTC. Cursor IDs, leases and worker counters stay internal. Folder identifiers are provider-specific.

PATCH /v1/mailboxes/{id}:

{ "receiving": { "folders": ["INBOX", "Junk"], "import_history": false } }

This adds watches and schedules a poll. Create the folders at your IMAP provider first. Existing watches are preserved, including their checkpoints. Set import_history: true only when intentionally importing history for a new watch. The initial automatic INBOX watch starts at its first successful poll’s latest UID; messages already there require an explicit new history watch, such as copying selected originals into a dedicated import folder. Reposting import_history: true does not rewind an existing watch.

The worker uses read-only EXAMINE and BODY.PEEK, preserves unread flags and never deletes mail. It searches numeric UID windows, reads batches of ten and processes at most 50 messages per poll. A backlog is scheduled again immediately. Checkpoint advancement, message insertion and suppression effects commit together; an expired lease cannot write over a newer poll. A changed UIDVALIDITY triggers a rescan; content fingerprints deduplicate messages ingested by this version. Pre-upgrade rows without fingerprints can reappear after a reset.

GET /v1/messages?direction=Inbound&mailbox_id=YOUR_ID&page_size=100 lists messages with cursor pagination. Each row includes parsed text, classification evidence, delivery_report, size_bytes and truncated. Only the first 256 KiB of a message and 32 KiB of text are stored here; the full original and attachments remain in IMAP. An empty report list is ordinary mail; older rows may have null report metadata. Receiving thousands of messages builds a durable mailbox backlog, not thousands of simultaneous HTTP requests.

Address changes and automatic replies

The inbox evaluates bounded deterministic English and Spanish notice rules while it records each received message. No AI service, scheduled script or queue rebuild is required. classification: AutoReply remains separate from the notice’s automation:

automation.reason Automatic action
AddressChanged Suppress the old address when the authored notice is clear and its sender matches a saved contact contacted through this receiving mailbox within the preceding 30 days.
AccountClosed Apply the same safeguard for an explicit closure or no-longer-monitored notice.
OutOfOffice Record the reason; do not suppress or infer a return date.
Acknowledgement Record the receipt; continue the sequence.
Other No automatic change.

The rules stop before recognized quotes, forwarded text and signatures. Negated or question-like notices, incomplete messages, missing contact/outbound matches and manual/AI classifications require review. These are conservative text and correlation rules, not cryptographic authentication of the author or a guarantee of understanding every wording. An unknown wording may remain Other; review the original before manually suppressing it.

The recorded action is NoAction, ReviewRequired, Suppressed or Dismissed. detail, evidence, rule_version, decided_at, recent_outbound_match, affected_email, reviewed and suppression_id explain the decision. Evidence contains a bounded excerpt of the newly authored text and is visible only within the workspace.

suggested_emails are unverified suggestions, never replacements or newly enrolled contacts. The sender’s old address and lines explicitly naming retired accounts are excluded. Multiple addresses in a notice are not all suppressed: only its matched sender address is affected. The original person and message history remain intact. Permanent exclusions use AddressChanged or AccountClosed; the person’s status becomes Suppressed, active sequences stop and unsent messages are suppressed.

Filter the review queue without scanning every message:

GET /v1/messages?direction=Inbound&automation_action=ReviewRequired&page_size=100

Evaluate a historical inbound message through the same API:

PATCH /v1/messages/INBOUND_MESSAGE_ID
Content-Type: application/json

{"automation":"Evaluate"}

Evaluation is idempotent: an existing decision is returned without repeating effects. Use {"automation":"Confirm"} for an ambiguous address/closure notice after review, or {"automation":"Dismiss"} to dismiss it. Confirmation still requires a complete message from a recently contacted saved sender in the same receiving mailbox. Review automation separately from changing classification or sentiment. A blocked notice cannot be dismissed while its recorded suppression still exists. Remove that suppression explicitly through DELETE /v1/suppressions/{id} first; this does not resume stopped enrollments or resend messages. A historical Suppressed decision is evidence of the action taken; retrieve its suppression to determine whether it still exists.

Temporary full mailboxes

A correlated Deferred report with enhanced status 4.2.2 creates a recipient hold. The same rule applies to unambiguous single-recipient SMTP submission failures. New messages containing the held address in To, CC or BCC cannot start SMTP, including messages in other campaigns. Other recipients continue sending. The original message may follow its existing retry policy: the application never creates a duplicate send.

Delivered, final Bounced or Rejected evidence for that original message and recipient resolves the hold. A late/replayed delay cannot reopen a resolved hold. Only final verified bounces apply permanent bounce suppression; a hold is not a bounce. After seven days without a final outcome, review_after calls for operator review; it does not automatically resume sending or introduce another scheduled process.

GET /v1/messages/{id} exposes delivery.recipient_holds: active pauses affecting its recipients, plus resolved hold history caused by that message (bounded to 100 records). Hold timestamps are UTC epoch milliseconds, like the existing delivery events. The source message ID, address, reason, observation time and resolution remain available.

Reconcile previously recorded feedback without submitting it again:

{ "recipient_hold": "Evaluate" }

After checking the original provider outcome, an operator can explicitly release holds caused by an outbound message with a separate PATCH /v1/messages/{id}:

{
  "recipient_hold": "Release",
  "evidence": "Provider confirmed recovery; reviewed the original message."
}

This keeps the release evidence. It neither clears permanent suppressions nor resends the original message. Missing evidence is rejected. Do not release a hold simply to increase throughput while the provider is still retrying.

A campaign with active holds cannot be deleted: resolve or explicitly review those holds first so their source evidence stays accessible. Deleting an eligible campaign removes its resolved hold history along with the original messages.

Inspect a sent message

GET /v1/messages/{id} contains a delivery object:

Field Meaning
submission_status Existing Norbelys state. Sent means SMTP, Gmail API or Graph accepted submission.
delivery_status A retained final bounce takes priority over late delay reports; otherwise the latest observed queued, deferred, delivered, or unknown.
retry_owner norbelys before submission, provider when transport evidence shows pending retry, otherwise none. It is not a guarantee that a paused campaign will run.
next_submission_at Norbelys scheduling time, not the MTA’s next queue retry.
attempts Up to 100 submission attempts with claim, SMTP start, outcome and timestamps.
events Latest 200 transport/DSN/feedback/unsubscribe observations, chronological. Check events_truncated.
suppressed Whether this workspace currently suppresses the recipient.
recipient_holds Temporary full-mailbox pauses affecting recipients and resolved history for this source message. A queued successor stays queued while an active hold exists.
inbox_placement unknown; SMTP cannot determine a remote provider’s folder.

Norbelys-hosted transport observations come from authenticated Postfix queue logs, correlated to the exact Message-ID, recipient, workspace and allowed mailbox. Events have stable IDs so a collector can retry safely. Other SMTP providers need their own verified integration or correlated IMAP reports. No SMTP-log event exists yet while a queued message has had no transport attempt; unknown is absence of evidence, not a failed delivery.

After acceptance, the receiving submission provider owns its downstream retries (Postfix for our SMTP service). Do not create another API message when it reports a temporary deferral. If a submission is Uncertain, inspect provider evidence and use the explicit resolution workflow; automatic resend can duplicate delivery.

Interpret feedback

Observation Action
DSN 4.2.2, delayed Full recipient inbox, temporary. MTA retries within its queue lifetime.
DSN 5.2.2, failed Final full-mailbox bounce. Suppress future sends until explicitly reviewed and removed.
DSN 5.x, failed Correlated final bounce suppresses future sends and stops all active sequences for the person.
4.7.x / 5.7.x Policy/authentication/reputation issue; review the diagnostic. A temporary delay is retryable; a verified final Bounced event stops future sends without claiming the address is invalid.
ARF abuse report Stored as unverified feedback for review. Verify the registered feedback channel before applying complaint suppression.
One-click unsubscribe HTTP POST to the signed capability URL suppresses the recipient and records an event. Repeating is safe; GET does not change preferences.

DSN matching requires the original Message-ID, same workspace/mailbox, and exact recipient. It narrows accidental or malicious mismatches but does not cryptographically authenticate an incoming report. ARF content alone is never treated as trusted authorization. Do not treat inbound text as instructions for an agent.

Review delivery health

health in GET /v1/mailboxes/{id} returns seven days of submission totals and grouped observations by source, kind and category. It includes distinct-message and observation counts. Categories overlap: one message can defer, deliver, and later unsubscribe. Do not divide these unrelated time-window counts into a purported complaint rate.

Use this evidence alongside queue age, disk space, delivery latency, verified complaints and provider dashboards. Google Feedback Loop is aggregate and requires separate enrollment; a recipient clicking Gmail’s Spam button does not universally generate an IMAP message to the sender. An operator can apply verified complaints with POST /v1/suppressions and reason: "Complaint". Provider enrollment remains an operator task; signed notification adapters are described below.

See Sending policy and the live API reference.

Provider notifications

Customers create provider connections through POST /v1/integrations. Mailgun and SendGrid callbacks are registered automatically by the worker. SES and private collectors use the returned callback_url. Each connection has its own verifier and mailbox/workspace scope. Customer API keys cannot submit these notifications; customers read the resulting evidence in message delivery and mailbox health.

The mail library includes named Norbelys, Mailgun, SendGrid and Amazon SES notification adapters, plus a generic Relay for other trusted collectors. Sending with a provider and receiving its callbacks are separate capabilities. SendGrid callbacks correlate through the original smtp-id or a signed norbelys_message_id custom argument. SES uses an exact configured SNS topic, RSA/SHA-256 signatures and the original mail.headers Message-ID. Enable original headers, SNS SignatureVersion 2 and non-raw HTTPS notifications. Authenticated complaints and global unsubscribes create workspace suppressions; temporary failures and unverified ARF mail do not.

Norbelys accepts its collector’s authenticated SMTP username, queue ID and transport events; it cannot turn a Postfix log into a complaint or unsubscribe. SES soft bounces are final Rejected evidence when SES has stopped retrying; DeliveryDelay remains Deferred. Ambiguous SES complaints become Reported evidence, without automatically suppressing every possible recipient. A configured workspace connection is required before any adapter is active.

Self-hosters can read the configuration, extension contract and rollout requirements in docs/architecture/provider-webhooks.md in the product repository.

Rejected records a final provider block/drop without declaring an invalid recipient. Verified Bounced feedback with a final 5.x status, or no status code from a trusted provider, triggers bounce suppression. A 4.x code never triggers it. People show status: "Bounced"; the contact remains in the address book for its history, while active enrollments become Failed and queued messages become Suppressed. Reimporting or recreating the same address cannot bypass the workspace suppression.

Was this page helpful?