Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions core/views/emails/_button.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/**
* The shared call-to-action button — "bulletproof" in the email sense.
*
* Built as a TABLE with the padding on the <a>, not a styled div: Outlook renders through Word,
* which ignores padding on block elements and would collapse a CSS button into bare underlined
* text. The `mso-padding-alt` + conditional wrapper give Outlook a real box to draw.
*
* Variables: $btnUrl, $btnLabel.
*/
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
$url = (string) $this->btnUrl;
$label = (string) $this->btnLabel;
if ($url === '' || $label === '') { return; }
?>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:28px 0;">
<tr>
<td align="center" class="tg-btn" style="border-radius:8px; background:#3459e6; mso-padding-alt:14px 28px;">
<a href="<?= $e($url) ?>"
style="display:inline-block; padding:14px 28px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; font-size:16px; font-weight:600; line-height:1; color:#ffffff; text-decoration:none; border-radius:8px;">
<?= $e($label) ?>
</a>
</td>
</tr>
</table>
33 changes: 33 additions & 0 deletions core/views/emails/backup.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/** Backup report. Variables: $ok, $filename, $size, $components, $error. */
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
$ok = (bool) $this->ok;
?>
<h1 class="tg-h1" style="margin:0 0 14px; font-size:24px; line-height:1.3; font-weight:700; color:#111827;">
Backup <?= $ok ? 'completed' : 'failed' ?>
</h1>

<?php // A status band rather than coloured words: scannable at a glance in a list of reports. ?>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 20px;">
<tr>
<td style="background:<?= $ok ? '#ecfdf5' : '#fef2f2' ?>; border:1px solid <?= $ok ? '#a7f3d0' : '#fecaca' ?>; border-left:4px solid <?= $ok ? '#059669' : '#dc2626' ?>; border-radius:8px; padding:14px 16px; font-size:15px; color:<?= $ok ? '#065f46' : '#991b1b' ?>;">
<strong><?= $e($this->filename) ?></strong><br />
<?= $ok ? 'Backup finished successfully.' : $e($this->error ?: 'unknown error') ?>
</td>
</tr>
</table>

<?php if ($ok): ?>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-size:15px;">
<tr>
<td class="tg-muted" width="120" style="padding:6px 0; color:#6b7280;">Size</td>
<td style="padding:6px 0;"><?= $e($this->size) ?></td>
</tr>
<tr>
<td class="tg-muted" style="padding:6px 0; color:#6b7280; vertical-align:top;">Components</td>
<td style="padding:6px 0;"><?= $e($this->components) ?></td>
</tr>
</table>
<?php endif; ?>
132 changes: 132 additions & 0 deletions core/views/emails/layout.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/**
* The shared transactional email layout.
*
* Every email Tiger sends renders inside this shell, so password resets, sign-in codes, signup
* verifications and backup reports all look like one product.
*
* Written to EMAIL rules, not web rules — they are not the same medium:
* - Table-based layout with role="presentation". Flex/grid and modern CSS are unusable in Outlook.
* - Styles are INLINE. Gmail strips <head><style> in several contexts, so anything load-bearing
* must survive without it; the <style> block only carries progressive extras (dark mode,
* small-screen tweaks) that are safe to lose.
* - 600px is the conventional safe width; below that, mobile clients scale instead of pan.
* - No external images. A remote logo is blocked by default in most clients and would render as a
* broken box on first open, so the wordmark is live text.
*
* Variables: $content (rendered body HTML), $siteName, $siteUrl, optional $preheader.
*/
$siteName = (string) ($this->siteName ?: 'Tiger');
$siteUrl = (string) ($this->siteUrl ?: '');
$preheader = (string) ($this->preheader ?: '');
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
$year = date('Y');
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title><?= $e($siteName) ?></title>
<style type="text/css">
/* Progressive only — everything load-bearing is inlined below. */
body { margin:0 !important; padding:0 !important; width:100% !important; }
table { border-collapse:collapse !important; }
img { border:0; line-height:100%; outline:none; text-decoration:none; -ms-interpolation-mode:bicubic; }
a { color:#3459e6; }
/* Outlook renders in Word: force a sane font instead of Times New Roman. */
#outlook a { padding:0; }
.ExternalClass { width:100%; }
.ExternalClass, .ExternalClass p, .ExternalClass td, .ExternalClass div { line-height:100%; }

@media only screen and (max-width:620px) {
.tg-wrap { width:100% !important; }
.tg-pad { padding-left:24px !important; padding-right:24px !important; }
.tg-h1 { font-size:22px !important; }
.tg-btn a { display:block !important; }
}

/* Dark mode: honored by Apple Mail and Outlook.com; others simply keep the light palette. */
@media (prefers-color-scheme: dark) {
.tg-body { background:#0f1115 !important; }
.tg-card { background:#171a21 !important; border-color:#2a2f3a !important; }
.tg-text { color:#e5e7eb !important; }
.tg-muted { color:#9aa1ac !important; }
.tg-h1 { color:#f3f4f6 !important; }
.tg-rule { border-color:#2a2f3a !important; }
.tg-panel { background:#1e222b !important; border-color:#2a2f3a !important; }
.tg-footer { color:#8b929c !important; }
}
</style>
</head>
<body class="tg-body" style="margin:0; padding:0; background:#f4f5f7; -webkit-font-smoothing:antialiased;">

<?php if ($preheader !== ''): ?>
<?php /* The inbox preview line. Hidden in the body, then padded so the client doesn't pull the
first paragraph in after it. */ ?>
<div style="display:none; font-size:1px; color:#f4f5f7; line-height:1px; max-height:0; max-width:0; opacity:0; overflow:hidden;">
<?= $e($preheader) ?>
<?= str_repeat('&#847;&zwnj;&nbsp;', 60) ?>
</div>
<?php endif; ?>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="tg-body" style="background:#f4f5f7;">
<tr>
<td align="center" style="padding:32px 12px;">

<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" class="tg-wrap" style="width:600px; max-width:600px;">

<!-- Wordmark -->
<tr>
<td align="left" style="padding:0 0 16px 4px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; font-size:17px; font-weight:700; letter-spacing:-0.2px; color:#111827;">
<?php if ($siteUrl !== ''): ?>
<a href="<?= $e($siteUrl) ?>" style="color:#111827; text-decoration:none;" class="tg-h1"><?= $e($siteName) ?></a>
<?php else: ?>
<span class="tg-h1"><?= $e($siteName) ?></span>
<?php endif; ?>
</td>
</tr>

<!-- Card -->
<tr>
<td class="tg-card" style="background:#ffffff; border:1px solid #e5e7eb; border-radius:12px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="tg-pad tg-text" style="padding:36px 40px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; font-size:16px; line-height:1.6; color:#1f2937;">
<?php // Already-rendered HTML from the content template — intentionally raw.
// Zend_View does NOT extract assigned vars into local scope, so this
// must be $this->content; a bare $content silently renders an empty shell. ?>
<?= $this->content ?>
</td>
</tr>
</table>
</td>
</tr>

<!-- Footer -->
<tr>
<td class="tg-pad" style="padding:20px 8px 0;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="tg-footer" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; font-size:13px; line-height:1.6; color:#6b7280;">
<?php if ($siteUrl !== ''): ?>
<a href="<?= $e($siteUrl) ?>" style="color:#6b7280; text-decoration:underline;"><?= $e(preg_replace('~^https?://~', '', $siteUrl)) ?></a><br />
<?php endif; ?>
&copy; <?= $e($year) ?> <?= $e($siteName) ?>. This is an automated message — please don&rsquo;t reply to it.
</td>
</tr>
</table>
</td>
</tr>

</table>

</td>
</tr>
</table>
</body>
</html>
20 changes: 20 additions & 0 deletions core/views/emails/otp.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/** Passwordless sign-in code. Variables: $code. */
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
?>
<h1 class="tg-h1" style="margin:0 0 14px; font-size:24px; line-height:1.3; font-weight:700; color:#111827;">Your sign-in code</h1>

<p style="margin:0 0 8px;">Use this code to finish signing in. It expires in <strong>10 minutes</strong>.</p>

<?php // Letter-spaced monospace in a panel: the code is the payload, so it gets read at a glance. ?>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;">
<tr>
<td class="tg-panel" align="center" style="background:#f3f4f6; border:1px solid #e5e7eb; border-radius:10px; padding:20px 12px;">
<span class="tg-h1" style="font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace; font-size:34px; font-weight:700; letter-spacing:8px; color:#111827;"><?= $e($this->code) ?></span>
</td>
</tr>
</table>

<p class="tg-muted" style="margin:0; font-size:13px; color:#6b7280;">If you didn&rsquo;t request this, you can ignore this email &mdash; no one can sign in without the code.</p>
24 changes: 24 additions & 0 deletions core/views/emails/register-verify.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/** Site-registration email verification. Variables: $url, $domain. */
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
$url = (string) $this->url;
?>
<h1 class="tg-h1" style="margin:0 0 14px; font-size:24px; line-height:1.3; font-weight:700; color:#111827;">Verify your Tiger install</h1>

<p style="margin:0 0 20px;">Confirm this email address to finish registering your site.</p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 4px;">
<tr>
<td class="tg-panel" style="background:#f9fafb; border:1px solid #e5e7eb; border-radius:8px; padding:14px 16px;">
<span class="tg-muted" style="font-size:13px; color:#6b7280;">Domain</span><br />
<strong style="font-size:15px;"><?= $e($this->domain) ?></strong>
</td>
</tr>
</table>

<?php $this->btnUrl = $url; $this->btnLabel = 'Verify my site'; echo $this->render('_button.phtml'); ?>

<p class="tg-muted" style="margin:0 0 8px; font-size:13px; color:#6b7280;">Or paste this link into your browser:</p>
<p style="margin:0; font-size:13px; word-break:break-all;"><a href="<?= $e($url) ?>" style="color:#3459e6;"><?= $e($url) ?></a></p>
23 changes: 23 additions & 0 deletions core/views/emails/reset.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/** Password reset. Variables: $url. */
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
$url = (string) $this->url;
?>
<h1 class="tg-h1" style="margin:0 0 14px; font-size:24px; line-height:1.3; font-weight:700; color:#111827;">Reset your password</h1>

<p style="margin:0 0 16px;">We received a request to reset your password. Choose a new one with the button below.</p>

<p style="margin:0;">This link expires in <strong>1 hour</strong>.</p>

<?php $this->btnUrl = $url; $this->btnLabel = 'Reset password'; echo $this->render('_button.phtml'); ?>

<p class="tg-muted" style="margin:0 0 8px; font-size:13px; color:#6b7280;">Or paste this link into your browser:</p>
<p style="margin:0 0 24px; font-size:13px; word-break:break-all;"><a href="<?= $e($url) ?>" style="color:#3459e6;"><?= $e($url) ?></a></p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="tg-rule" style="border-top:1px solid #e5e7eb; font-size:0; line-height:0;">&nbsp;</td></tr>
</table>

<p class="tg-muted" style="margin:20px 0 0; font-size:13px; color:#6b7280;">If you didn&rsquo;t request this, you can safely ignore this email &mdash; your password won&rsquo;t change.</p>
45 changes: 45 additions & 0 deletions core/views/emails/test.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/**
* The "Send test" email from System -> Email SMTP.
*
* It deliberately reports HOW it was delivered: the whole point of the test is to prove a specific
* provider/host works, and an admin comparing two configurations needs to know which one arrived.
*
* Variables: $via, $sentAt.
*/
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
?>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 18px;">
<tr>
<td style="background:#ecfdf5; border:1px solid #a7f3d0; border-radius:999px; padding:6px 14px; font-size:13px; font-weight:600; color:#065f46;">
&#10003;&nbsp; Delivery confirmed
</td>
</tr>
</table>

<h1 class="tg-h1" style="margin:0 0 14px; font-size:24px; line-height:1.3; font-weight:700; color:#111827;">Outgoing mail is working</h1>

<p style="margin:0 0 20px;">This is a test message from your <?= $e($this->siteName) ?> install. Because it reached your inbox, password resets, verification links and notifications will reach your users too.</p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-size:15px;">
<?php if ($this->via): ?>
<tr>
<td class="tg-muted" width="110" style="padding:6px 0; color:#6b7280;">Sent via</td>
<td style="padding:6px 0;"><strong><?= $e($this->via) ?></strong></td>
</tr>
<?php endif; ?>
<?php if ($this->sentAt): ?>
<tr>
<td class="tg-muted" style="padding:6px 0; color:#6b7280;">Sent at</td>
<td style="padding:6px 0;"><?= $e($this->sentAt) ?></td>
</tr>
<?php endif; ?>
</table>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:22px;">
<tr><td class="tg-rule" style="border-top:1px solid #e5e7eb; font-size:0; line-height:0;">&nbsp;</td></tr>
</table>

<p class="tg-muted" style="margin:20px 0 0; font-size:13px; color:#6b7280;">You received this because someone used <strong>Send test</strong> on the System &rsaquo; Email SMTP screen. No action is needed.</p>
21 changes: 21 additions & 0 deletions core/views/emails/verify.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers.
/** Signup email verification — the first email a new customer ever receives. Variables: $url. */
$e = function ($s) { return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); };
$url = (string) $this->url;
?>
<h1 class="tg-h1" style="margin:0 0 14px; font-size:24px; line-height:1.3; font-weight:700; color:#111827;">Confirm your email address</h1>

<p style="margin:0 0 16px;">Welcome to <?= $e($this->siteName) ?>! Confirm your email address to activate your account.</p>

<?php $this->btnUrl = $url; $this->btnLabel = 'Verify my email'; echo $this->render('_button.phtml'); ?>

<p class="tg-muted" style="margin:0 0 8px; font-size:13px; color:#6b7280;">Or paste this link into your browser:</p>
<p style="margin:0 0 24px; font-size:13px; word-break:break-all;"><a href="<?= $e($url) ?>" style="color:#3459e6;"><?= $e($url) ?></a></p>

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="tg-rule" style="border-top:1px solid #e5e7eb; font-size:0; line-height:0;">&nbsp;</td></tr>
</table>

<p class="tg-muted" style="margin:20px 0 0; font-size:13px; color:#6b7280;">If you didn&rsquo;t create this account, you can safely ignore this email.</p>
15 changes: 9 additions & 6 deletions library/Tiger/Backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -447,14 +447,17 @@ protected static function _notify($ok, $filename, array $data, array $opts)
try {
$host = $_SERVER['HTTP_HOST'] ?? gethostname();
$subject = ($ok ? '✅ Backup succeeded' : '⚠️ Backup FAILED') . ' — ' . $host;
$body = $ok
? '<p>Backup <strong>' . htmlspecialchars($filename) . '</strong> completed.</p><p>Size: '
. self::hsize((int) ($data['size'] ?? 0)) . '<br>Components: ' . htmlspecialchars(implode(', ', $data['components'] ?? [])) . '</p>'
: '<p>Backup <strong>' . htmlspecialchars($filename) . '</strong> <span style="color:#c00">failed</span>.</p><p>Reason: '
. htmlspecialchars((string) ($data['error'] ?? 'unknown')) . '</p>';
$mail = new Tiger_Mail();
foreach ($to as $addr) { $mail->to($addr); }
$mail->subject($subject)->html($body)->send();
$mail->subject($subject)
->template('backup', [
'ok' => $ok,
'filename' => $filename,
'size' => self::hsize((int) ($data['size'] ?? 0)),
'components' => implode(', ', $data['components'] ?? []),
'error' => (string) ($data['error'] ?? ''),
])
->send();
} catch (Throwable $e) {
Tiger_Log::warn('backup.notify.failed', ['error' => $e->getMessage()]);
}
Expand Down
Loading
Loading