phacility/phabricator · info · PhabricatorMetaMTAPermanentFailureException

Unit Test (Permanent)

Error message

Unit Test (Permanent)

What it means

PhabricatorMailTestAdapter is a fake mailer for unit tests and manual verification. Its setFailPermanently(true) flag arms sendMessage() to throw PhabricatorMetaMTAPermanentFailureException with 'Unit Test (Permanent)', simulating a permanent delivery failure so you can observe the mail queue marking a message as failed immediately instead of retrying. Seeing this means the mailer in play is the test adapter with failure injection enabled - it is test tooling, not a production error.

Source

Thrown at src/applications/metamta/adapter/PhabricatorMailTestAdapter.php:58

  protected function validateOptions(array $options) {
    PhutilTypeSpec::checkMap($options, array());
  }

  public function newDefaultOptions() {
    return array();
  }

  public function supportsMessageIDHeader() {
    return $this->supportsMessageID;
  }

  public function getGuts() {
    return $this->guts;
  }

  public function sendMessage(PhabricatorMailExternalMessage $message) {
    if ($this->failPermanently) {
      throw new PhabricatorMetaMTAPermanentFailureException(
        pht('Unit Test (Permanent)'));
    }

    if ($this->failTemporarily) {
      throw new Exception(
        pht('Unit Test (Temporary)'));
    }

    switch ($message->getMessageType()) {
      case PhabricatorMailEmailMessage::MESSAGETYPE:
        $guts = $this->newEmailGuts($message);
        break;
      case PhabricatorMailSMSMessage::MESSAGETYPE:
        $guts = $this->newSMSGuts($message);
        break;
    }

    $guts['did-send'] = true;

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. If you expected delivery: this path only runs when setFailPermanently(true) was called explicitly - find the caller; production mailers are configured in cluster.mailers with a real adapter type.
  2. If testing: assert the outcome you instrumented - the mail should transition to 'fail' status immediately, with no worker retries.
  3. Inspect outbound state with the MetaMTA mail UI or 'bin/mail' workflows to confirm the permanent-failure semantics you were testing.

Example fix

// before (test)
$mailer = id(new PhabricatorMailTestAdapter())->setFailPermanently(true);

// after (test)
$mailer = id(new PhabricatorMailTestAdapter());
// or keep the flag and assert the permanent failure explicitly:
//   assert($mail->getStatus() == PhabricatorMailOutboundStatus::STATUS_FAIL);
Defensive patterns

Strategy: validation

Validate before calling

// Deployment guard: production installs must not enable failure injection.
foreach (PhabricatorMetaMTAMail::newMailers(array()) as $mailer) {
  if ($mailer->getAdapterType() === 'test' && $env !== 'test') {
    warn('Test mailer configured outside a test environment.');
  }
}

Prevention

When it happens

Trigger: Unit tests or scripts that construct the test adapter, call setFailPermanently(true), and then trigger a send (direct sendMessage() or through the mail worker); verifying that permanent failures set PhabricatorMailOutboundStatus::STATUS_FAIL without retry.

Common situations: Writing/running Phabricator mail-queue tests; QA exercising the permanent-failure path. If it appears outside tests, some code path built a test adapter with the flag set - production config of type 'test' never sets it (the adapter accepts no options at all).

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/528179559135b5d2. Report an issue: GitHub.