663fde3909
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>
208 lines
7.0 KiB
PHP
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">✝ {$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">
|
|
© <?= 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;
|
|
}
|