assertNoInjection($from, 'expediteur'); $this->assertNoInjection($to, 'destinataire'); $t = $this->transport; try { $t->open($host, $port, 15); $this->expect($t->readReply(), 220, 'greeting'); $this->command('EHLO ' . $this->heloName, 250, 'EHLO'); $this->command('STARTTLS', 220, 'STARTTLS'); $t->enableCrypto(); // Re-EHLO obligatoire apres bascule TLS (la session repart de zero). $this->command('EHLO ' . $this->heloName, 250, 'EHLO TLS'); $this->command('AUTH LOGIN', 334, 'AUTH LOGIN'); $this->command(base64_encode($user), 334, 'AUTH user'); $this->command(base64_encode($password), 235, 'AUTH password'); $this->command('MAIL FROM:<' . $from . '>', 250, 'MAIL FROM'); $this->command('RCPT TO:<' . $to . '>', 250, 'RCPT TO'); $this->command('DATA', 354, 'DATA'); // Corps + terminateur ".". $t->write($message . "\r\n.\r\n"); $this->expect($t->readReply(), 250, 'corps du message'); $t->write("QUIT\r\n"); // La fermeture (221) n'est pas bloquante : le message est deja accepte. $t->readReply(); } finally { $t->close(); } } private function command(string $line, int $expected, string $stage): void { $this->transport->write($line . "\r\n"); $this->expect($this->transport->readReply(), $expected, $stage); } private function assertNoInjection(string $address, string $label): void { if (preg_match('/[\r\n]/', $address) === 1) { throw new RuntimeException( sprintf('SMTP : adresse %s invalide (saut de ligne interdit)', $label), ); } } private function expect(string $reply, int $code, string $stage): void { $got = (int) substr(ltrim($reply), 0, 3); if ($got !== $code) { // On ne journalise pas le corps : il peut contenir le lien de reset. throw new RuntimeException( sprintf('SMTP %s : attendu %d, recu "%s"', $stage, $code, trim($reply)), ); } } }