emailverifier.dev
Posts

SMTP 550 5.1.1: What Recipient Address Rejected Means

| 4 min read | Usama Ejaz
SMTP 550 5.1.1 cover with a rejected RCPT TO command and one mailbox.

550 5.1.1 at the RCPT TO stage is a permanent recipient-address failure. The receiving server is saying that it cannot deliver to the specified destination mailbox. Retrying the same address unchanged is not the right response.

C: MAIL FROM:<sender@example.net>
S: 250 2.1.0 Sender OK
C: RCPT TO:<missing@example.com>
S: 550 5.1.1 Recipient address rejected: User unknown

The exact command, three-digit reply, enhanced status code, and response text all matter. A different 550, such as 5.7.1, can indicate a policy or security rejection instead of a missing recipient.

Read the response in four pieces

  1. RCPT TO identifies the stage. The server is evaluating the envelope recipient.
  2. 550 is a permanent negative completion reply under SMTP.
  3. 5.1.1 is the enhanced status for a bad destination mailbox address.
  4. User unknown is human-readable detail. Keep it in diagnostics, but automate from the codes.

RFC 5321 section 4.2.3 defines the persistent and transient reply classes. RFC 3463 section 3.2 defines X.1.1 as a bad destination mailbox address.

Preserve the complete transcript when diagnosing a delivery or verification result. A bare “550 error” does not say whether the rejection concerned the recipient, relay authorization, message policy, sender authentication, or another condition.

Parse the reply without treating every 550 alike

Create smtp-reply.mjs:

const replyPattern = /^(?<code>\d{3})[ -](?:(?<enhanced>[245]\.\d{1,3}\.\d{1,3})\s+)?(?<message>.*)$/

export function parseSmtpReply(line) {
  const match = replyPattern.exec(line.trim())
  if (!match?.groups) throw new Error('Invalid SMTP reply line')

  return {
    code: Number(match.groups.code),
    enhanced: match.groups.enhanced ?? null,
    message: match.groups.message
  }
}

export function classifyRecipientReply(line) {
  const reply = parseSmtpReply(line)

  if (reply.code === 550 && reply.enhanced === '5.1.1') {
    return {
      outcome: 'recipient_rejected',
      retry: false,
      evidence: reply
    }
  }

  if (reply.code >= 400 && reply.code < 500) {
    return {
      outcome: 'temporary_failure',
      retry: true,
      evidence: reply
    }
  }

  if (reply.code >= 500 && reply.enhanced?.startsWith('5.7.')) {
    return {
      outcome: 'policy_rejection',
      retry: false,
      evidence: reply
    }
  }

  if (reply.code >= 500) {
    return {
      outcome: 'permanent_failure',
      retry: false,
      evidence: reply
    }
  }

  if (reply.code >= 200 && reply.code < 300) {
    return {
      outcome: 'accepted_at_this_stage',
      retry: false,
      evidence: reply
    }
  }

  return {
    outcome: 'unclassified',
    retry: false,
    evidence: reply
  }
}

The function deliberately says accepted_at_this_stage for a 2xx response. SMTP acceptance during a probe or delivery transaction does not guarantee inbox placement, later delivery, or recipient engagement.

Test recipient, temporary, and policy failures

Create smtp-reply.test.mjs:

import assert from 'node:assert/strict'
import test from 'node:test'

import { classifyRecipientReply } from './smtp-reply.mjs'

test('550 5.1.1 is a permanent recipient rejection', () => {
  const result = classifyRecipientReply(
    '550 5.1.1 Recipient address rejected: User unknown'
  )

  assert.equal(result.outcome, 'recipient_rejected')
  assert.equal(result.retry, false)
})

test('450 4.1.1 remains temporary', () => {
  const result = classifyRecipientReply(
    '450 4.1.1 Recipient temporarily unavailable'
  )

  assert.equal(result.outcome, 'temporary_failure')
  assert.equal(result.retry, true)
})

test('550 5.7.1 is not mislabeled as a missing mailbox', () => {
  const result = classifyRecipientReply(
    '550 5.7.1 Message rejected by policy'
  )

  assert.equal(result.outcome, 'policy_rejection')
})

Run node --test.

Decide whether to retry, correct, or remove

EvidenceDefault actionReason
550 5.1.1 at RCPT TOAsk for a corrected address or suppress the recipient.The destination mailbox was permanently rejected.
450 4.x.xRetry later with a bounded schedule.A 4xx response is transient.
550 5.7.xInvestigate policy, authentication, reputation, or content context.The enhanced code is not a bad-mailbox code.
Timeout or connection failureKeep the mailbox result unknown and retry according to policy.No recipient response was received.

One server can still be misconfigured, and a mailbox can be restored later. “Permanent” describes how SMTP tells the current sender to handle this transaction. Keep the timestamp and server response so a later correction or re-verification can replace the old evidence.

Map SMTP evidence to an API result

You do not need to run an SMTP probe from an application server. Outbound port restrictions, provider defenses, catch-all behavior, and temporary policy blocks make a homegrown boolean unreliable.

Create an emailverifier.dev account and project, then copy its API key. Make one address request:

curl https://emailverifier.dev/api/v1/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_PROJECT_KEY" \
  -d '{"email":"person@example.com"}'

A clear recipient rejection can support status: undeliverable and action: block with a mailbox_rejected signal. A temporary failure, timeout, catch-all response, or policy block may instead remain status: unknown and action: review.

The complete API status set is deliverable, risky, undeliverable, and unknown. Application actions are allow, review, and block. The SMTP verification guide owns the full command sequence; this page owns the narrower 550 5.1.1 diagnosis.

Continue reading