diff --git a/src/app/api/informations/[id]/image/route.ts b/src/app/api/informations/[id]/image/route.ts
new file mode 100644
index 0000000..05b2538
--- /dev/null
+++ b/src/app/api/informations/[id]/image/route.ts
@@ -0,0 +1,42 @@
+import { NextResponse } from "next/server";
+import { getInformation } from "@/lib/informations";
+
+export const dynamic = "force-dynamic";
+
+/**
+ * Sert l'image de couverture d'une information **publiée**, en octets.
+ *
+ * Elle est stockée en data-URI, ce qui convient au site mais pas aux e-mails :
+ * Gmail et Outlook suppriment les ``. Cette route donne au
+ * gabarit une URL ordinaire, et évite au passage de recopier plusieurs
+ * méga-octets dans chaque ligne de la file d'attente.
+ *
+ * Seules les informations publiées répondent : un brouillon ne doit pas fuiter
+ * par une URL devinable.
+ */
+const DATA_URI = /^data:(image\/(?:png|jpeg|jpg|webp|gif));base64,([A-Za-z0-9+/=\s]+)$/;
+
+export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id: raw } = await params;
+ const id = Number(raw);
+ if (!Number.isInteger(id) || id <= 0) return new NextResponse("Introuvable", { status: 404 });
+
+ const information = await getInformation(id);
+ if (!information || information.status !== "published" || !information.imageUrl) {
+ return new NextResponse("Introuvable", { status: 404 });
+ }
+
+ const match = DATA_URI.exec(information.imageUrl);
+ if (!match) return new NextResponse("Introuvable", { status: 404 });
+
+ const bytes = Buffer.from(match[2].replace(/\s+/g, ""), "base64");
+ return new NextResponse(bytes, {
+ headers: {
+ "Content-Type": match[1],
+ "Content-Length": String(bytes.length),
+ // L'image d'une information ne change pas : les clients mail peuvent la
+ // garder, et un message relu six mois plus tard l'affiche encore.
+ "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800",
+ },
+ });
+}
diff --git a/src/app/backend/actions.ts b/src/app/backend/actions.ts
index b2a0dac..acf393c 100644
--- a/src/app/backend/actions.ts
+++ b/src/app/backend/actions.ts
@@ -54,9 +54,10 @@ import {
} from "@/lib/mail-accounts";
import type { MailProvider } from "@/db/schema";
import { sendNow } from "@/lib/mailer";
-import { cancelMailMessage, logSentMail, retryMailMessage } from "@/lib/mail-outbox";
-import { selfRecipient } from "@/lib/mail-recipients";
-import { buildCredentialsEmail, buildGeneralEmail } from "@/lib/email-templates";
+import { cancelMailMessage, logSentMail, queueMails, retryMailMessage } from "@/lib/mail-outbox";
+import { activeMemberRecipients, selfRecipient } from "@/lib/mail-recipients";
+import { buildCredentialsEmail, buildGeneralEmail, buildInformationEmail } from "@/lib/email-templates";
+import { richTextToPlain } from "@/lib/rich-text";
import { emailBrand, getSiteSettings } from "@/lib/site-settings";
import { siteUrl } from "@/lib/social-accounts";
import { markInformationsRead as markRead } from "@/lib/informations";
@@ -1477,8 +1478,18 @@ export async function saveInformation(
return { id: created.id };
}
+/**
+ * Les refus de diffusion passent par une redirection avec message plutôt que
+ * par un retour : le bouton « Publier » est un `