diff --git a/app/routes/app.settings.tsx b/app/routes/app.settings.tsx
index 033ec63..d9ca14e 100644
--- a/app/routes/app.settings.tsx
+++ b/app/routes/app.settings.tsx
@@ -14,6 +14,12 @@ import {
storeUploadedLogo,
} from "../services/invoice/logoCache.server";
import { RichTextEditor } from "../components/RichTextEditor";
+import {
+ DEFAULT_EMAIL_BODY_DE,
+ DEFAULT_EMAIL_BODY_EN,
+ DEFAULT_EMAIL_SUBJECT_DE,
+ DEFAULT_EMAIL_SUBJECT_EN,
+} from "../services/invoice/emailTemplates";
interface SettingsFieldErrors {
vatId?: string;
@@ -376,13 +382,13 @@ export default function SettingsRoute() {
@@ -390,13 +396,13 @@ export default function SettingsRoute() {
@@ -425,6 +431,8 @@ const EMAIL_VARS = [
{ token: "{{dueDate}}" },
{ token: "{{companyName}}" },
{ token: "{{ownerName}}" },
+ { token: "{{shopEmail}}" },
+ { token: "{{shopWebsite}}" },
];
interface FieldProps {
diff --git a/app/services/invoice/email.server.ts b/app/services/invoice/email.server.ts
index 4104ff8..a73982a 100644
--- a/app/services/invoice/email.server.ts
+++ b/app/services/invoice/email.server.ts
@@ -4,6 +4,13 @@ import type { ShopSettings } from "@prisma/client";
import db from "../../db.server";
import { getStrings, pickLanguage } from "./i18n";
+import {
+ DEFAULT_EMAIL_BODY_DE,
+ DEFAULT_EMAIL_BODY_EN,
+ DEFAULT_EMAIL_SUBJECT_DE,
+ DEFAULT_EMAIL_SUBJECT_EN,
+} from "./emailTemplates";
+import { STORED_LOGO_SENTINEL } from "./logoCache.constants";
export interface SendInvoiceEmailArgs {
shopDomain: string;
@@ -73,20 +80,20 @@ export async function sendInvoiceEmail(
totalGross: totals.totalGross ?? "",
});
- const customSubject = language === "en" ? settings.emailSubjectEn : settings.emailSubjectDe;
- const subject = customSubject
- ? renderTemplate(customSubject, vars)
- : `${t.invoice} ${invoice.invoiceNumber}` +
- (settings.companyName ? ` — ${settings.companyName}` : "");
+ const customSubject =
+ (language === "en" ? settings.emailSubjectEn : settings.emailSubjectDe) ||
+ (language === "en" ? DEFAULT_EMAIL_SUBJECT_EN : DEFAULT_EMAIL_SUBJECT_DE);
+ const subject = renderTemplate(customSubject, vars);
- const customBodyHtml = language === "en" ? settings.emailBodyHtmlEn : settings.emailBodyHtmlDe;
- const body = customBodyHtml
- ? renderHtmlBody(renderTemplate(customBodyHtml, vars))
- : renderEmailBody({
- settings,
- invoiceNumber: invoice.invoiceNumber,
- language,
- });
+ const customBodyHtml =
+ (language === "en" ? settings.emailBodyHtmlEn : settings.emailBodyHtmlDe) ||
+ (language === "en" ? DEFAULT_EMAIL_BODY_EN : DEFAULT_EMAIL_BODY_DE);
+ const body = renderHtmlBody(renderTemplate(customBodyHtml, vars));
+
+ // If the rendered body references the inline logo, attach it.
+ const inlineLogo = body.html.includes("cid:invoice-logo")
+ ? await loadInlineLogo(args.shopDomain, settings)
+ : null;
// Download the PDF (Shopify Files URLs are public CDN URLs).
let pdfBytes: Uint8Array;
@@ -122,6 +129,16 @@ export async function sendInvoiceEmail(
content: Buffer.from(pdfBytes),
contentType: "application/pdf",
},
+ ...(inlineLogo
+ ? [
+ {
+ filename: "logo",
+ content: Buffer.from(inlineLogo.bytes),
+ contentType: inlineLogo.contentType,
+ cid: "invoice-logo",
+ },
+ ]
+ : []),
],
});
@@ -232,6 +249,8 @@ interface TemplateVars {
dueDate: string;
companyName: string;
ownerName: string;
+ shopEmail: string;
+ shopWebsite: string;
}
function buildTemplateVars(args: {
@@ -251,6 +270,8 @@ function buildTemplateVars(args: {
dueDate: dueMs ? new Date(dueMs).toLocaleDateString() : "",
companyName: args.settings.companyName,
ownerName: args.settings.ownerName,
+ shopEmail: args.settings.email,
+ shopWebsite: args.settings.website,
};
}
@@ -308,3 +329,28 @@ function parseTotals(json: string): InvoiceTotalsSnapshot {
return {};
}
}
+
+/**
+ * Resolves the shop logo bytes for inline embedding. Returns null if no
+ * logo is configured or the lookup fails — the email is still sent, just
+ * without the inline image (the alt text remains visible).
+ */
+async function loadInlineLogo(
+ shopDomain: string,
+ settings: ShopSettings,
+): Promise<{ bytes: Uint8Array; contentType: string } | null> {
+ if (!settings.logoUrl) return null;
+ try {
+ if (settings.logoUrl === STORED_LOGO_SENTINEL) {
+ const cached = await db.logoCache.findUnique({ where: { shopDomain } });
+ if (!cached) return null;
+ return { bytes: new Uint8Array(cached.bytes), contentType: cached.contentType };
+ }
+ const res = await fetch(settings.logoUrl);
+ if (!res.ok) return null;
+ const ct = res.headers.get("content-type") ?? "image/png";
+ return { bytes: new Uint8Array(await res.arrayBuffer()), contentType: ct };
+ } catch {
+ return null;
+ }
+}
diff --git a/app/services/invoice/emailTemplates.ts b/app/services/invoice/emailTemplates.ts
new file mode 100644
index 0000000..b3a00b4
--- /dev/null
+++ b/app/services/invoice/emailTemplates.ts
@@ -0,0 +1,52 @@
+/**
+ * Default invoice email templates per language. Used when the user hasn't
+ * customised them in settings. Variables ({{invoiceNumber}}, etc.) are
+ * substituted by `renderTemplate` at send time.
+ *
+ * The shop logo is rendered as an inline attachment with content-id
+ * `invoice-logo`; the email sender attaches the logo bytes automatically
+ * when the template (or any custom template) references that cid.
+ */
+
+const DE_HTML = `\
+
{{companyName}}
+Danke für deinen Einkauf!
+
+Die Rechnung befindet sich im Anhang.
+
+
+Bei Überweisung bitte die Rechnungs-Nummer als Referenz verwenden:
+{{invoiceNumber}}
+Besten Dank!
+
+
+
+
+
+✉ Kontakt
+🌐 {{shopWebsite}}
+
`;
+
+const EN_HTML = `\
+{{companyName}}
+Thank you for your purchase!
+
+Please find the invoice attached.
+
+
+When paying by bank transfer, please use the invoice number as the reference:
+{{invoiceNumber}}
+Thanks a lot!
+
+
+
+
+
+✉ Contact
+🌐 {{shopWebsite}}
+
`;
+
+export const DEFAULT_EMAIL_SUBJECT_DE = "Rechnung {{invoiceNumber}} – {{companyName}}";
+export const DEFAULT_EMAIL_SUBJECT_EN = "Invoice {{invoiceNumber}} – {{companyName}}";
+export const DEFAULT_EMAIL_BODY_DE = DE_HTML;
+export const DEFAULT_EMAIL_BODY_EN = EN_HTML;
diff --git a/data/mail_template.png b/data/mail_template.png
new file mode 100644
index 0000000..9699793
Binary files /dev/null and b/data/mail_template.png differ