Files
Rosary/includes/mailer.php
T
pguzman 663fde3909 Initial commit — Rosary Presenter App
Full source for loveandrosary.com: slide-based Rosary/novena/Divine Mercy
Chaplet presentation tool with multi-user roles, SVG bead ring, audio uploads,
donate strip, and public session profiles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:44:08 -07:00

208 lines
7.0 KiB
PHP

<?php
/**
* includes/mailer.php — lightweight SMTP mailer using stream_socket_client().
* Falls back to PHP mail() when smtp_host is not configured.
*/
/**
* Send an email.
*
* @param string $to_email Recipient email address
* @param string $to_name Recipient display name
* @param string $subject Email subject
* @param string $html HTML body
* @param string $text Plain-text body (auto-generated from HTML if empty)
* @return bool True on success, false on failure
*/
function send_email(string $to_email, string $to_name, string $subject, string $html, string $text = ''): bool {
$smtp_host = get_setting('smtp_host');
$smtp_port = (int) get_setting('smtp_port', '587');
$smtp_user = get_setting('smtp_user');
$smtp_pass = get_setting('smtp_pass');
$smtp_from = get_setting('smtp_from');
$smtp_from_name = get_setting('smtp_from_name', 'Rosary Presenter');
if ($text === '') {
$text = strip_tags(preg_replace('/<br\s*\/?>/i', "\n", $html));
}
// Fall back to PHP mail() if SMTP is not configured
if ($smtp_host === '') {
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
if ($smtp_from !== '') {
$headers .= 'From: ' . _mail_encode_name($smtp_from_name) . ' <' . $smtp_from . ">\r\n";
}
return @mail($to_email, $subject, $html, $headers);
}
try {
return _smtp_send($smtp_host, $smtp_port, $smtp_user, $smtp_pass, $smtp_from, $smtp_from_name, $to_email, $to_name, $subject, $html, $text);
} catch (Throwable $e) {
error_log('Mailer error: ' . $e->getMessage());
return false;
}
}
/**
* Return a simple styled HTML email wrapper.
*/
function email_template(string $title, string $body_html): string {
$site_name = get_setting('site_name', 'Rosary Presenter');
return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{$title}</title>
</head>
<body style="margin:0;padding:0;background:#f4f4f5;font-family:system-ui,-apple-system,sans-serif">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 20px">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;max-width:600px;width:100%">
<tr>
<td style="background:#1e3a5f;padding:24px 32px;text-align:center">
<h1 style="margin:0;color:#ffffff;font-size:22px;font-weight:600">&#x271D; {$site_name}</h1>
</td>
</tr>
<tr>
<td style="padding:32px">
{$body_html}
</td>
</tr>
<tr>
<td style="background:#f4f4f5;padding:20px 32px;text-align:center;color:#6b7280;font-size:13px">
&copy; <?= date('Y') ?> {$site_name}. This email was sent automatically.
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>
HTML;
}
// ---------------------------------------------------------------------------
// Internal SMTP helpers
// ---------------------------------------------------------------------------
function _smtp_send(
string $host, int $port,
string $user, string $pass,
string $from_addr, string $from_name,
string $to_addr, string $to_name,
string $subject, string $html, string $text
): bool {
$use_ssl = ($port === 465);
$socket_addr = ($use_ssl ? 'ssl://' : 'tcp://') . $host . ':' . $port;
$errno = 0;
$errstr = '';
$sock = stream_socket_client($socket_addr, $errno, $errstr, 15);
if (!$sock) {
throw new RuntimeException("SMTP connect failed ({$errno}): {$errstr}");
}
stream_set_timeout($sock, 15);
_smtp_expect($sock, 220);
_smtp_cmd($sock, 'EHLO ' . gethostname());
$ehlo = _smtp_read_all($sock);
// STARTTLS for port 587
if (!$use_ssl && strpos($ehlo, 'STARTTLS') !== false) {
_smtp_cmd($sock, 'STARTTLS');
_smtp_expect($sock, 220);
if (!stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new RuntimeException('STARTTLS negotiation failed');
}
_smtp_cmd($sock, 'EHLO ' . gethostname());
_smtp_read_all($sock);
}
// AUTH LOGIN
if ($user !== '') {
_smtp_cmd($sock, 'AUTH LOGIN');
_smtp_expect($sock, 334);
_smtp_cmd($sock, base64_encode($user));
_smtp_expect($sock, 334);
_smtp_cmd($sock, base64_encode($pass));
_smtp_expect($sock, 235);
}
_smtp_cmd($sock, 'MAIL FROM:<' . $from_addr . '>');
_smtp_expect($sock, 250);
_smtp_cmd($sock, 'RCPT TO:<' . $to_addr . '>');
_smtp_expect($sock, [250, 251]);
_smtp_cmd($sock, 'DATA');
_smtp_expect($sock, 354);
$boundary = 'b_' . bin2hex(random_bytes(8));
$date = date('r');
$msg_id = bin2hex(random_bytes(12)) . '@' . gethostname();
$headers = "Date: {$date}\r\n";
$headers .= 'From: ' . _mail_encode_name($from_name) . ' <' . $from_addr . ">\r\n";
$headers .= 'To: ' . _mail_encode_name($to_name) . ' <' . $to_addr . ">\r\n";
$headers .= 'Subject: ' . _mail_encode_name($subject) . "\r\n";
$headers .= "Message-ID: <{$msg_id}>\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$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($text) . "\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($html) . "\r\n";
$body .= "--{$boundary}--\r\n";
// Dot-stuff the body
$body = preg_replace('/^\.$/m', '..', $body);
fwrite($sock, $headers . "\r\n" . $body . "\r\n.\r\n");
_smtp_expect($sock, 250);
_smtp_cmd($sock, 'QUIT');
fclose($sock);
return true;
}
function _smtp_cmd($sock, string $cmd): void {
fwrite($sock, $cmd . "\r\n");
}
function _smtp_read_all($sock): string {
$data = '';
while (!feof($sock)) {
$line = fgets($sock, 512);
if ($line === false) break;
$data .= $line;
// Last line of multi-line response has a space after the code
if (strlen($line) >= 4 && $line[3] === ' ') break;
}
return $data;
}
function _smtp_expect($sock, $codes): void {
$response = _smtp_read_all($sock);
$code = (int)substr(trim($response), 0, 3);
$expected = (array)$codes;
if (!in_array($code, $expected, true)) {
throw new RuntimeException("SMTP unexpected response {$code}: " . trim($response));
}
}
function _mail_encode_name(string $name): string {
if ($name === '') return '';
// RFC 2047 encode if needed
if (preg_match('/[^\x20-\x7E]/', $name) || strpbrk($name, '"\\,;<>@') !== false) {
return '=?UTF-8?B?' . base64_encode($name) . '?=';
}
return $name;
}