mirror of
https://github.com/myronblair/tomtomgames-app
synced 2026-06-30 17:49:57 -05:00
184 lines
6.7 KiB
PHP
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
|
|
);
|
|
}
|