Improve email branding and SMTP envelope
This commit is contained in:
@@ -70,7 +70,7 @@ export function bookingConfirmedEmail(
|
||||
extras.push(renderButton("Meeting betreten", args.online.joinUrl));
|
||||
extras.push(
|
||||
`<p style="margin:0 0 4px;color:#66505f;font-size:14px;"><strong>Meeting-Passwort:</strong></p>` +
|
||||
`<p style="margin:0 0 20px;color:#66505f;font-size:20px;letter-spacing:0.08em;font-family:'Courier New',monospace;">${escapeHtml(args.online.password)}</p>`,
|
||||
`<p style="margin:0 0 20px;color:#66505f;font-size:20px;letter-spacing:0.08em;font-family:'Antropos',Georgia,'Times New Roman',serif;">${escapeHtml(args.online.password)}</p>`,
|
||||
);
|
||||
}
|
||||
if (args.onsite) {
|
||||
|
||||
+18
-11
@@ -5,6 +5,8 @@ const colors = {
|
||||
creamLight: "#fbf6f1",
|
||||
};
|
||||
|
||||
const emailFont = "'Antropos', Georgia, 'Times New Roman', serif";
|
||||
|
||||
export function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
@@ -17,7 +19,7 @@ export function renderButton(label: string, href: string): string {
|
||||
return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:24px auto;">
|
||||
<tr>
|
||||
<td style="border-radius:999px;background:${colors.mauveDark};">
|
||||
<a href="${href}" style="display:inline-block;padding:14px 32px;color:#ffffff;text-decoration:none;font-size:14px;font-weight:600;letter-spacing:0.02em;">
|
||||
<a href="${href}" style="display:inline-block;padding:14px 32px;color:#ffffff;text-decoration:none;font-family:${emailFont};font-size:14px;font-weight:600;letter-spacing:0.02em;">
|
||||
${escapeHtml(label)}
|
||||
</a>
|
||||
</td>
|
||||
@@ -28,7 +30,7 @@ export function renderButton(label: string, href: string): string {
|
||||
export function renderFactBox(rows: { label: string; value: string }[]): string {
|
||||
const cells = rows
|
||||
.map(
|
||||
(row, i) => `<p style="margin:0 ${i === rows.length - 1 ? "0" : "0 12px"} 0;color:${colors.plum};font-size:14px;">
|
||||
(row, i) => `<p style="margin:0 ${i === rows.length - 1 ? "0" : "0 12px"} 0;color:${colors.plum};font-family:${emailFont};font-size:14px;">
|
||||
<strong>${escapeHtml(row.label)}:</strong><br />${escapeHtml(row.value)}
|
||||
</p>`,
|
||||
)
|
||||
@@ -45,24 +47,29 @@ export function renderEmailShell(args: { preheader?: string; bodyHtml: string; f
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<body style="margin:0;padding:0;background:${colors.creamLight};font-family:Georgia,'Times New Roman',serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${colors.creamLight};padding:32px 16px;">
|
||||
<head>
|
||||
<style>
|
||||
@font-face { font-family: 'Antropos'; src: url('https://anouma.org/fonts/baar-antropos.ttf') format('truetype'); font-style: normal; font-weight: 100 900; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:${colors.creamLight};font-family:${emailFont};">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${colors.creamLight};padding:32px 16px;font-family:${emailFont};">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:24px;overflow:hidden;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:24px;overflow:hidden;font-family:${emailFont};">
|
||||
<tr>
|
||||
<td style="background:${colors.mauveDark};padding:28px 32px;text-align:center;">
|
||||
<span style="color:#ffffff;font-size:22px;letter-spacing:0.12em;font-weight:600;">ANOUMA</span>
|
||||
<td style="background:${colors.mauveDark};padding:28px 32px;text-align:center;font-family:${emailFont};">
|
||||
<span style="color:#ffffff;font-family:${emailFont};font-size:22px;letter-spacing:0.12em;font-weight:600;">ANOUMA</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:32px;">
|
||||
<td style="padding:32px;font-family:${emailFont};">
|
||||
${bodyHtml}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:20px 32px;background:${colors.cream};text-align:center;">
|
||||
<span style="color:${colors.plum};font-size:12px;">${escapeHtml(footerText)}</span>
|
||||
<td style="padding:20px 32px;background:${colors.cream};text-align:center;font-family:${emailFont};">
|
||||
<span style="color:${colors.plum};font-family:${emailFont};font-size:12px;">${escapeHtml(footerText)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -76,5 +83,5 @@ export function renderEmailShell(args: { preheader?: string; bodyHtml: string; f
|
||||
export function paragraph(text: string, opts: { italic?: boolean; small?: boolean } = {}): string {
|
||||
const size = opts.small ? "13px" : "16px";
|
||||
const style = opts.italic ? "font-style:italic;" : "";
|
||||
return `<p style="margin:0 0 20px;color:${colors.plum};font-size:${size};line-height:1.6;${style}">${escapeHtml(text)}</p>`;
|
||||
return `<p style="margin:0 0 20px;color:${colors.plum};font-family:${emailFont};font-size:${size};line-height:1.6;${style}">${escapeHtml(text)}</p>`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PayloadEmailAdapter } from "payload";
|
||||
import { getMailFrom, getMailTransport } from "./transport";
|
||||
import { getMailEnvelopeFrom, getMailFrom, getMailTransport } from "./transport";
|
||||
|
||||
/** Connects Payload's built-in auth emails to the same SMTP transport as ANOUMA's own emails. */
|
||||
export const payloadEmailAdapter: PayloadEmailAdapter = () => ({
|
||||
@@ -9,7 +9,8 @@ export const payloadEmailAdapter: PayloadEmailAdapter = () => ({
|
||||
sendEmail: async (message) => {
|
||||
return getMailTransport().sendMail({
|
||||
...message,
|
||||
from: message.from || getMailFrom(),
|
||||
from: getMailFrom(),
|
||||
envelope: { from: getMailEnvelopeFrom() },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,34 +21,39 @@ export function renderReminderEmailHtml(data: ReminderEmailData): string {
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<body style="margin:0;padding:0;background:${colors.creamLight};font-family:Georgia,'Times New Roman',serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${colors.creamLight};padding:32px 16px;">
|
||||
<head>
|
||||
<style>
|
||||
@font-face { font-family: 'Antropos'; src: url('https://anouma.org/fonts/baar-antropos.ttf') format('truetype'); font-style: normal; font-weight: 100 900; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:${colors.creamLight};font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${colors.creamLight};padding:32px 16px;font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:24px;overflow:hidden;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:24px;overflow:hidden;font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<tr>
|
||||
<td style="background:${colors.mauveDark};padding:28px 32px;text-align:center;">
|
||||
<span style="color:#ffffff;font-size:22px;letter-spacing:0.12em;font-weight:600;">ANOUMA</span>
|
||||
<td style="background:${colors.mauveDark};padding:28px 32px;text-align:center;font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<span style="color:#ffffff;font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:22px;letter-spacing:0.12em;font-weight:600;">ANOUMA</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:32px;">
|
||||
<p style="margin:0 0 20px;color:${colors.plum};font-size:16px;line-height:1.6;">
|
||||
<td style="padding:32px;font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<p style="margin:0 0 20px;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:16px;line-height:1.6;">
|
||||
Hallo ${escapeHtml(recipientName)},
|
||||
</p>
|
||||
<p style="margin:0 0 24px;color:${colors.plum};font-size:18px;line-height:1.6;font-style:italic;">
|
||||
<p style="margin:0 0 24px;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:18px;line-height:1.6;font-style:italic;">
|
||||
Dein Online-Termin bei ANOUMA beginnt in ${minutesBefore} Minuten.
|
||||
</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${colors.cream};border-radius:16px;">
|
||||
<tr>
|
||||
<td style="padding:20px 24px;">
|
||||
<p style="margin:0 0 12px;color:${colors.plum};font-size:14px;">
|
||||
<td style="padding:20px 24px;font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<p style="margin:0 0 12px;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:14px;">
|
||||
<strong>Termin:</strong><br />${escapeHtml(eventTitle)}
|
||||
</p>
|
||||
<p style="margin:0 0 12px;color:${colors.plum};font-size:14px;">
|
||||
<p style="margin:0 0 12px;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:14px;">
|
||||
<strong>Datum:</strong><br />${escapeHtml(dateLabel)}
|
||||
</p>
|
||||
<p style="margin:0;color:${colors.plum};font-size:14px;">
|
||||
<p style="margin:0;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:14px;">
|
||||
<strong>Uhrzeit:</strong><br />${escapeHtml(timeLabel)} Uhr
|
||||
</p>
|
||||
</td>
|
||||
@@ -57,26 +62,26 @@ export function renderReminderEmailHtml(data: ReminderEmailData): string {
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:28px auto;">
|
||||
<tr>
|
||||
<td style="border-radius:999px;background:${colors.mauveDark};">
|
||||
<a href="${joinUrl}" style="display:inline-block;padding:14px 32px;color:#ffffff;text-decoration:none;font-size:14px;font-weight:600;letter-spacing:0.02em;">
|
||||
<a href="${joinUrl}" style="display:inline-block;padding:14px 32px;color:#ffffff;text-decoration:none;font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:14px;font-weight:600;letter-spacing:0.02em;">
|
||||
Meeting betreten
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:0 0 4px;color:${colors.plum};font-size:14px;">
|
||||
<p style="margin:0 0 4px;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:14px;">
|
||||
<strong>Meeting-Passwort:</strong>
|
||||
</p>
|
||||
<p style="margin:0 0 20px;color:${colors.plum};font-size:20px;letter-spacing:0.08em;font-family:'Courier New',monospace;">
|
||||
<p style="margin:0 0 20px;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:20px;letter-spacing:0.08em;">
|
||||
${escapeHtml(meetingPassword)}
|
||||
</p>
|
||||
<p style="margin:0;color:${colors.plum};font-size:13px;line-height:1.6;">
|
||||
<p style="margin:0;color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:13px;line-height:1.6;">
|
||||
Bitte halte dein Passwort bereit. Beim Betreten des Meetings wirst du außerdem nach deinem Namen gefragt.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:20px 32px;background:${colors.cream};text-align:center;">
|
||||
<span style="color:${colors.plum};font-size:12px;">Räume für Verbindung - mit dir selbst, miteinander und mit der Natur.</span>
|
||||
<td style="padding:20px 32px;background:${colors.cream};text-align:center;font-family:'Antropos',Georgia,'Times New Roman',serif;">
|
||||
<span style="color:${colors.plum};font-family:'Antropos',Georgia,'Times New Roman',serif;font-size:12px;">Räume für Verbindung - mit dir selbst, miteinander und mit der Natur.</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getMailFrom, getMailTransport } from "./transport";
|
||||
import { getMailEnvelopeFrom, getMailFrom, getMailTransport } from "./transport";
|
||||
|
||||
export async function sendEmail(
|
||||
to: string,
|
||||
@@ -8,6 +8,7 @@ export async function sendEmail(
|
||||
const transport = getMailTransport();
|
||||
await transport.sendMail({
|
||||
from: getMailFrom(),
|
||||
envelope: { from: getMailEnvelopeFrom() },
|
||||
to,
|
||||
subject: template.subject,
|
||||
html: template.html,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { getMailFrom, getMailTransport } from "./transport";
|
||||
import { getMailEnvelopeFrom, getMailFrom, getMailTransport } from "./transport";
|
||||
import { renderReminderEmailHtml, renderReminderEmailText, type ReminderEmailData } from "./reminderTemplate";
|
||||
|
||||
export async function sendReminderEmail(to: string, data: ReminderEmailData) {
|
||||
const transport = getMailTransport();
|
||||
await transport.sendMail({
|
||||
from: getMailFrom(),
|
||||
envelope: { from: getMailEnvelopeFrom() },
|
||||
to,
|
||||
subject: `Dein Online-Termin bei ANOUMA beginnt in ${data.minutesBefore} Minuten`,
|
||||
html: renderReminderEmailHtml(data),
|
||||
|
||||
+11
-1
@@ -30,5 +30,15 @@ export function getMailTransport(): nodemailer.Transporter {
|
||||
}
|
||||
|
||||
export function getMailFrom(): string {
|
||||
return process.env.SMTP_FROM || "ANOUMA <support@anouma.org>";
|
||||
const configured = (process.env.SMTP_FROM || process.env.SMTP_USER || "support@anouma.org").trim();
|
||||
const unquoted = configured.replace(/^("|')(.*)\1$/, "$2").trim();
|
||||
|
||||
// Keep the address in the message header, but show only ANOUMA in mail clients.
|
||||
if (unquoted.includes("<") && unquoted.includes(">")) return unquoted;
|
||||
return `ANOUMA <${unquoted}>`;
|
||||
}
|
||||
|
||||
/** Envelope sender used by strict SMTP relays for sender ownership checks. */
|
||||
export function getMailEnvelopeFrom(): string {
|
||||
return process.env.SMTP_USER || "support@anouma.org";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user