Sans serveur SMTP, PlanFlow ne peut ni inviter un salarié, ni notifier une publication de planning, ni délivrer l'information due au retour d'un arrêt. L'application n'expédie rien par elle-même : elle se connecte au serveur du client, pour que les messages partent de son domaine et que les salariés reconnaissent l'expéditeur. Le mot de passe SMTP est traité comme le NIR et l'IBAN — chiffré au repos avec la même clé, jamais renvoyé à l'écran (la page ne le charge même pas chiffré), jamais recopié dans un message d'erreur ni dans le journal d'audit. Les erreurs SMTP passent par une passe de masquage : les serveurs renvoient volontiers la commande AUTH en clair. Laisser le champ vide conserve le mot de passe enregistré, sans quoi changer un numéro de port casserait l'envoi. Enregistrer et éprouver sont deux gestes distincts : toute modification remet le réglage en « non vérifié », car un réglage non éprouvé n'est pas un réglage, c'est une intention. L'envoi de test vérifie d'abord la connexion — ce qui sépare une adresse de serveur fautive d'un mot de passe faux — puis expédie réellement. Les messages sont rendus en texte et en HTML depuis le même contenu, sans image, script ni ressource distante : la charte de télémétrie vaut aussi pour le courrier, un pixel de suivi dans un message RH est une collecte que personne n'a acceptée. Le journal d'envoi retient le destinataire et l'issue, pas le corps du message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
71 lines
2.2 KiB
SQL
71 lines
2.2 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "EmailKind" AS ENUM ('TEST', 'INVITATION', 'PASSWORD_RESET', 'PLANNING_PUBLISHED', 'TIMEOFF_DECISION', 'LEAVE_NOTICE');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "EmailStatus" AS ENUM ('SENT', 'FAILED');
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "EmailSettings" (
|
|
"id" TEXT NOT NULL,
|
|
"accountId" TEXT NOT NULL,
|
|
"host" TEXT NOT NULL,
|
|
"port" INTEGER NOT NULL DEFAULT 587,
|
|
"secure" BOOLEAN NOT NULL DEFAULT false,
|
|
"username" TEXT,
|
|
"passwordEnc" BYTEA,
|
|
"fromName" TEXT NOT NULL,
|
|
"fromAddress" TEXT NOT NULL,
|
|
"replyTo" TEXT,
|
|
"verifiedAt" TIMESTAMP(3),
|
|
"lastError" TEXT,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "EmailSettings_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "EmailLog" (
|
|
"id" TEXT NOT NULL,
|
|
"accountId" TEXT NOT NULL,
|
|
"to" TEXT NOT NULL,
|
|
"subject" TEXT NOT NULL,
|
|
"kind" "EmailKind" NOT NULL,
|
|
"status" "EmailStatus" NOT NULL,
|
|
"error" TEXT,
|
|
"messageId" TEXT,
|
|
"membershipId" TEXT,
|
|
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "EmailLog_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "EmailSettings_accountId_key" ON "EmailSettings"("accountId");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "EmailSettings_accountId_idx" ON "EmailSettings"("accountId");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "EmailLog_accountId_sentAt_idx" ON "EmailLog"("accountId", "sentAt");
|
|
|
|
-- Isolation : toute table portant accountId doit porter sa politique.
|
|
DO $$
|
|
DECLARE t text;
|
|
BEGIN
|
|
FOREACH t IN ARRAY ARRAY['EmailSettings','EmailLog']
|
|
LOOP
|
|
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
|
|
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', t);
|
|
EXECUTE format(
|
|
'CREATE POLICY tenant_isolation ON %I USING ("accountId" = planflow_current_account())', t);
|
|
EXECUTE format(
|
|
'CREATE POLICY tenant_insert ON %I FOR INSERT WITH CHECK ("accountId" = planflow_current_account())', t);
|
|
END LOOP;
|
|
END $$;
|
|
|
|
-- Le journal d'envoi atteste de ce qui a été transmis : il ne se réécrit pas.
|
|
CREATE TRIGGER email_log_append_only
|
|
BEFORE UPDATE OR DELETE ON "EmailLog"
|
|
FOR EACH ROW EXECUTE FUNCTION planflow_deny_write();
|