host = $host; $this->port = $port; $this->user = $user; $this->pass = $pass; $this->fromEmail = $fromEmail; $this->fromName = $fromName; $this->debug = $debug; } public function send(string $toEmail, string $toName, string $subject, string $textBody, string $htmlBody = ''): bool { $errno = 0; $errstr = ''; $socket = fsockopen("tcp://{$this->host}", $this->port, $errno, $errstr, 15); if (!$socket) { $this->log[] = "Connect failed: $errstr ($errno)"; return false; } stream_set_timeout($socket, 15); try { // Read greeting $this->expect($socket, 220, "greeting"); // EHLO $this->send_cmd($socket, "EHLO " . gethostname()); $ehlo = $this->read_response($socket); if (substr($ehlo, 0, 3) !== '250') { throw new Exception("EHLO failed: $ehlo"); } // STARTTLS $this->send_cmd($socket, "STARTTLS"); $this->expect($socket, 220, "STARTTLS"); // Upgrade to TLS if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { throw new Exception("TLS upgrade failed"); } // EHLO again after TLS $this->send_cmd($socket, "EHLO " . gethostname()); $ehlo2 = $this->read_response($socket); if (substr($ehlo2, 0, 3) !== '250') { throw new Exception("EHLO2 failed: $ehlo2"); } // AUTH LOGIN $this->send_cmd($socket, "AUTH LOGIN"); $this->expect($socket, 334, "AUTH LOGIN prompt"); $this->send_cmd($socket, base64_encode($this->user)); $this->expect($socket, 334, "Username prompt"); $this->send_cmd($socket, base64_encode($this->pass)); $this->expect($socket, 235, "AUTH success"); // MAIL FROM $this->send_cmd($socket, "MAIL FROM:<{$this->fromEmail}>"); $this->expect($socket, 250, "MAIL FROM"); // RCPT TO $this->send_cmd($socket, "RCPT TO:<{$toEmail}>"); $this->expect($socket, 250, "RCPT TO"); // DATA $this->send_cmd($socket, "DATA"); $this->expect($socket, 354, "DATA"); // Build message $boundary = 'boundary_' . md5(uniqid()); $fromHdr = $this->encodeName($this->fromName) . " <{$this->fromEmail}>"; $toHdr = $this->encodeName($toName) . " <{$toEmail}>"; $subjHdr = $this->encodeSubject($subject); $msgId = '<' . time() . '.' . rand(1000,9999) . '@' . $this->host . '>'; $headers = "From: $fromHdr\r\n"; $headers .= "To: $toHdr\r\n"; $headers .= "Subject: $subjHdr\r\n"; $headers .= "Message-ID: $msgId\r\n"; $headers .= "Date: " . date('r') . "\r\n"; $headers .= "MIME-Version: 1.0\r\n"; $headers .= "X-Mailer: TomTomGames/1.0\r\n"; if ($htmlBody) { $headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n"; $body = "--$boundary\r\n"; $body .= "Content-Type: text/plain; charset=UTF-8\r\n"; $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n"; $body .= quoted_printable_encode($textBody) . "\r\n"; $body .= "--$boundary\r\n"; $body .= "Content-Type: text/html; charset=UTF-8\r\n"; $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n"; $body .= quoted_printable_encode($htmlBody) . "\r\n"; $body .= "--$boundary--"; } else { $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; $headers .= "Content-Transfer-Encoding: quoted-printable\r\n"; $body = quoted_printable_encode($textBody); } // Dot-stuff and send $message = $headers . "\r\n" . $body; $message = preg_replace('/^\./m', '..', $message); fwrite($socket, $message . "\r\n.\r\n"); $this->expect($socket, 250, "Message accepted"); // QUIT $this->send_cmd($socket, "QUIT"); fclose($socket); return true; } catch (Exception $e) { $this->log[] = "Error: " . $e->getMessage(); try { $this->send_cmd($socket, "QUIT"); } catch(Exception $_) {} fclose($socket); return false; } } private function send_cmd($socket, string $cmd): void { if ($this->debug) $this->log[] = ">>> $cmd"; fwrite($socket, $cmd . "\r\n"); } private function read_response($socket): string { $response = ''; while ($line = fgets($socket, 512)) { if ($this->debug) $this->log[] = "<<< " . trim($line); $response .= $line; if (isset($line[3]) && $line[3] === ' ') break; // Multi-line ends when 4th char is space } return trim($response); } private function expect($socket, int $code, string $context): void { $resp = $this->read_response($socket); if (substr($resp, 0, 3) !== (string)$code) { throw new Exception("Expected $code at $context, got: $resp"); } } private function encodeName(string $name): string { if (preg_match('/[^\x20-\x7E]/', $name) || strpbrk($name, '"<>()')) { return '=?UTF-8?B?' . base64_encode($name) . '?='; } return '"' . addslashes($name) . '"'; } private function encodeSubject(string $subject): string { if (preg_match('/[^\x20-\x7E]/', $subject)) { return '=?UTF-8?B?' . base64_encode($subject) . '?='; } return $subject; } public function getLog(): array { return $this->log; } } /** * Factory — returns a ready-to-use mailer using config constants. */ function mailer(): SmtpMailer { return new SmtpMailer( SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM, SMTP_FROM_NAME ); }