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
- 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.
- If testing: assert the outcome you instrumented - the mail should transition to 'fail' status immediately, with no worker retries.
- 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
- Only call setFailPermanently() inside test code that asserts the failed-mail state.
- Keep type 'test' mailers on development installs only; audit cluster.mailers before promoting config.
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
- Unit Test (Temporary)
- err:exception
- Adapter ("%s") is configured for medium "%s", but this is no
- Mailer ("%s") is attempting to access unknown option ("%s").
- Failed to JSON decode response.
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/528179559135b5d2.
Report an issue: GitHub.