Skip to content

bug(smtp): RCPT TO rejection kills entire send instead of skipping invalid recipient #49

Description

@usernane

Bug Description

In SMTPServer::sendCommand(), any response code >= 400 from the previous command causes the next sendCommand() call to throw immediately:

if ($this->lastResponseCode >= 400) {
    throw new SMTPException(...);
}

This is incorrect for RCPT TO commands. In SMTP, a 5xx response to RCPT TO is a per-recipient rejection — not a session failure. The client should continue sending RCPT TO for remaining recipients and only fail if all are rejected.

Steps to Reproduce

  1. Create an email with multiple recipients: valid@example.com, invalid@example.com, also-valid@example.com
  2. The SMTP server rejects invalid@example.com with 550 User not found
  3. The next sendCommand() call (for also-valid@example.com) throws SMTPException without even being sent

Expected Behavior

  • Rejected RCPT TO addresses should be collected/logged but not abort the session
  • Remaining recipients should still be attempted
  • DATA should proceed if at least one recipient was accepted
  • Only throw if all recipients were rejected
  • QUIT should always be sent to cleanly close the connection

Actual Behavior

  • First RCPT TO rejection aborts the entire send
  • Remaining valid recipients never receive the message
  • QUIT is never sent, leaving the TCP connection dangling until server timeout
  • No way to know which recipients were accepted vs rejected

WebFiori Version

v2.1.1

PHP Version

8.1+

Operating System

Linux

Additional Context

Suggested approach — modify receiversCommandHelper() to handle rejections gracefully:

private function receiversCommandHelper($type): int {
    $server = $this->getSMTPServer();
    $accepted = 0;

    foreach ($this->receiversArr[$type] as $address => $name) {
        $server->sendCommand('RCPT TO: <' . $address . '>');

        if ($server->getLastResponseCode() < 400) {
            $accepted++;
        }
        // Log rejection but don't throw
    }

    return $accepted;
}

Then in send(), check total accepted count and throw only if zero.

This requires sendCommand() to not throw on the next call after a 4xx/5xx — the check should be contextual rather than blanket.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions