Files
tomtomgames-app/includes/smtp.php
T

184 lines
6.7 KiB
PHP

<?php
/**
* TomTomGames SMTP Mailer
* Sends email via SMTP using PHP's socket functions.
* Compatible with Outlook/Office365, Gmail, and standard SMTP servers.
*/
class SmtpMailer {
private string $host;
private int $port;
private string $user;
private string $pass;
private string $fromEmail;
private string $fromName;
private bool $debug;
private array $log = [];
public function __construct(
string $host,
int $port,
string $user,
string $pass,
string $fromEmail,
string $fromName,
bool $debug = false
) {
$this->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
);
}