VulCode
Blog

PHPMailer: QUIT no longer ends up inside an interrupted message

When sending broke off in the middle of the DATA command, PHPMailer sent QUIT, the server took it as another line of the message and kept waiting. Now the connection is simply closed in that case.

DualFroz - VulCode CEODualFroz - VulCode CEO1 min read
PHPMailer: QUIT no longer ends up inside an interrupted message - DualFroz - VulCode CEO
Title in the repository
Don't send QUIT while DATA is in progress
Merged
4 Oct 2026
Written up here
6 Oct 2026

The note was written 2 days after the change was merged.

PHPMailer
Technologies:PHPSMTP

What was broken

In SMTP, after the DATA command the server treats every following line as message content until it sees a line with a single dot. If sending broke off at exactly that point, say because of an exception or a timeout, and the application then closed the connection with quit(), PHPMailer sent a QUIT command. The server did not see the end of the session in it, only another line of the email, so it never replied, and the client waited for an answer until the time limit ran out. Issue #2994 described the problem.

What we changed

We followed the idea from the discussion in #3121: the SMTP class remembers the last command it sent, and quit() checks it before sending anything.

php
if ($this->state === 'DATA') {
    //The server would take QUIT as part of an unfinished message, so just close the connection
    $noerror = true;
} else {
    $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
}

sendCommand() records the state on every command and close() clears it. We pointed out one limitation in the pull request: a DATA command the server rejected also leaves the state at DATA, and we offered to track only accepted commands if the maintainer preferred that.

How we tested it

A new test, testQuitDuringData, opens a connection, sends MAIL and RCPT, starts DATA without sending the message, as if the send had been interrupted, and calls quit(). It then checks the client debug output to confirm that QUIT was not sent and that the connection is closed.

The result

The change was merged on 4 October 2026. Applications that clean up the connection after a failed send no longer hang until the timeout or append QUIT to an abandoned message.

The classic PHP library for sending email, used by WordPress and many other systems.

Language
PHP
GitHub stars
22,310

The same case before and after the change

On the example of the note: what came out before the fix and what comes out now.

The order from the testQuitDuringData test
MAIL
RCPT
DATA
quit()

DATA started without sending a body, as if the send had broken off, and then the connection cleaned up.

Before the change
QUIT

The server took QUIT as another line of the message and did not answer, while the client waited for the timeout (issue #2994).

After the change

QUIT is not sent and the connection is closed. The test checks this in the client log.

What it taught us

SMTP has states, and a command sent in the wrong one becomes part of the message. When sending mail we always check the connection's state before closing it.

From the change and the test in pull request #3362.

Questions about change #3362

Where the change is, how we tested it and who merged it.