host = $host; $this->port = $port; $this->username = $username; $this->password = $password; $this->encryption = strtolower($encryption); } public function setDebug($debug) { $this->debug = $debug; } public function setLogFunction($func) { $this->logFunction = $func; } private function log($message) { if ($this->logFunction && is_callable($this->logFunction)) { call_user_func($this->logFunction, $message); } if ($this->debug) { error_log($message); } } public function getLastError() { return $this->lastError; } /** * Sends an email via SMTP */ public function send($to, $subject, $body, $fromEmail, $fromName = '', $replyTo = '', $bcc = []) { try { // Connect to the server if (!$this->connect()) { return false; } // Send EHLO if (!$this->ehlo()) { $this->disconnect(); return false; } // Start TLS if necessary if ($this->encryption === 'tls') { if (!$this->startTls()) { $this->disconnect(); return false; } // After STARTTLS, send EHLO again if (!$this->ehlo()) { $this->disconnect(); return false; } } // authentication if (!$this->authenticate()) { $this->disconnect(); return false; } // Send email if (!$this->sendMail($to, $subject, $body, $fromEmail, $fromName, $replyTo, $bcc)) { $this->disconnect(); return false; } $this->disconnect(); return true; } catch (Exception $e) { $this->lastError = $e->getMessage(); $this->disconnect(); return false; } } private function connect() { $host = $this->host; // SSL prefix for port 465 if ($this->encryption === 'ssl') { $host = 'ssl://' . $host; } $this->log("Connecting to $host:$this->port ..."); $this->socket = @fsockopen($host, $this->port, $errno, $errstr, 30); if (!$this->socket) { $this->lastError = "Connection failed: $errstr ($errno)"; $this->log("ERROR: " . $this->lastError); return false; } $this->log("Connection established, waiting for greeting..."); // Read server greeting $response = $this->getResponse(); if (substr($response, 0, 3) !== '220') { $this->lastError = "Invalid server response: $response"; $this->log("ERROR: " . $this->lastError); return false; } $this->log("Server greeting OK"); return true; } private function disconnect() { if ($this->socket) { $this->sendCommand("QUIT"); fclose($this->socket); $this->socket = null; } } private function ehlo() { $hostname = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost'; // Never let a spoofed Host/SERVER_NAME inject SMTP commands via CR/LF. $hostname = preg_replace('/[^A-Za-z0-9.\-\[\]:]/', '', (string)$hostname); if ($hostname === '') $hostname = 'localhost'; $response = $this->sendCommand("EHLO $hostname"); return substr($response, 0, 3) === '250'; } private function startTls() { $response = $this->sendCommand("STARTTLS"); if (substr($response, 0, 3) !== '220') { $this->lastError = "STARTTLS failed: $response"; return false; } // Enable TLS encryption $crypto = stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); if (!$crypto) { $this->lastError = "Failed to enable TLS encryption"; return false; } return true; } private function authenticate() { // AUTH LOGIN $response = $this->sendCommand("AUTH LOGIN"); if (substr($response, 0, 3) !== '334') { $this->lastError = "AUTH LOGIN failed: $response"; return false; } // Username (Base64) $response = $this->sendCommand(base64_encode($this->username)); if (substr($response, 0, 3) !== '334') { $this->lastError = "Username rejected: $response"; return false; } // Password (Base64) $response = $this->sendCommand(base64_encode($this->password)); if (substr($response, 0, 3) !== '235') { $this->lastError = "Authentication failed: $response"; return false; } return true; } private function normalizeRecipients($value) { $rawItems = is_array($value) ? $value : explode(',', (string)$value); $recipients = []; foreach ($rawItems as $item) { foreach (explode(',', (string)$item) as $email) { $email = trim(str_replace(["\r", "\n", "\t"], '', (string)$email)); if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) { $recipients[] = $email; } } } return array_values(array_unique($recipients)); } private function formatRecipientHeader(array $recipients) { return implode(', ', array_map(function ($email) { return '<' . $email . '>'; }, $recipients)); } private function sendMail($to, $subject, $body, $fromEmail, $fromName, $replyTo, $bcc = []) { $toRecipients = $this->normalizeRecipients($to); $bccRecipients = $this->normalizeRecipients($bcc); if (empty($toRecipients)) { $this->lastError = 'No valid recipient address'; return false; } // MAIL FROM $response = $this->sendCommand("MAIL FROM:<$fromEmail>"); if (substr($response, 0, 3) !== '250') { $this->lastError = "MAIL FROM rejected: $response"; return false; } // BCC recipients are envelope recipients only and are not written to headers. foreach (array_merge($toRecipients, $bccRecipients) as $recipient) { $response = $this->sendCommand("RCPT TO:<$recipient>"); if (!in_array(substr($response, 0, 3), ['250', '251'], true)) { $this->lastError = "RCPT TO rejected for $recipient: $response"; return false; } } // DATA $response = $this->sendCommand("DATA"); if (substr($response, 0, 3) !== '354') { $this->lastError = "DATA rejected: $response"; return false; } // Email headers and body $headers = []; $headers[] = "Date: " . date('r'); $headers[] = "From: " . ($fromName ? "=?UTF-8?B?" . base64_encode($fromName) . "?= <$fromEmail>" : $fromEmail); $headers[] = "To: " . $this->formatRecipientHeader($toRecipients); $headers[] = "Subject: =?UTF-8?B?" . base64_encode($subject) . "?="; if ($replyTo) { $headers[] = "Reply-To: <$replyTo>"; } $headers[] = "MIME-Version: 1.0"; $headers[] = "Content-Type: text/plain; charset=UTF-8"; $headers[] = "Content-Transfer-Encoding: 8bit"; $headers[] = "X-Mailer: SmtpMailer/1.0"; $message = implode("\r\n", $headers) . "\r\n\r\n" . $body; // Escape dots at beginning of lines (SMTP protocol) $message = str_replace("\n.", "\n..", $message); // Send message and terminate with . fwrite($this->socket, $message . "\r\n.\r\n"); $response = $this->getResponse(); if (substr($response, 0, 3) !== '250') { $this->lastError = "Email not accepted: $response"; return false; } return true; } private function sendCommand($command) { // Don't log password $logCmd = (strpos($command, base64_encode($this->password)) !== false) ? '[PASSWORD]' : $command; $this->log("SMTP > $logCmd"); fwrite($this->socket, $command . "\r\n"); return $this->getResponse(); } private function getResponse() { $response = ''; while ($line = fgets($this->socket, 515)) { $response .= $line; // Check if last line (4th character is a space) if (isset($line[3]) && $line[3] === ' ') { break; } } $this->log("SMTP < " . trim($response)); return $response; } }