Checkpoint: Ajout de la possibilité d'envoyer des emails via SMTP directement (sans Resend) :
**Modifications de la base de données** : - Ajout des champs SMTP dans la table `emailConfig` : smtpHost, smtpPort, smtpSecure, smtpUser, smtpPassword **Backend** : - Installation de `nodemailer` pour l'envoi SMTP - Création de `server/_core/smtpSender.ts` avec les fonctions d'envoi SMTP et de test de connexion - Modification de `server/_core/emailSender.ts` pour supporter les deux méthodes (Resend et SMTP) - Priorité donnée à SMTP si configuré, sinon fallback sur Resend **Frontend** : - Mise à jour de `AdminEmailConfig.tsx` pour permettre le choix entre Resend et SMTP - Ajout d'un sélecteur de méthode d'envoi (Resend API / SMTP Direct) - Formulaire de configuration SMTP avec : * Hôte SMTP (ex: smtp.gmail.com) * Port (587, 465, 25) * Sécurité (TLS, SSL, None) * Identifiants (utilisateur/mot de passe) * Email expéditeur **Compatibilité** : - Support des principaux fournisseurs SMTP (Gmail, Outlook, serveurs SMTP personnalisés) - Instructions pour les mots de passe d'application Gmail - Mode simulation toujours disponible pour les tests
This commit is contained in:
9
.manus/db/db-query-1764329367533.json
Normal file
9
.manus/db/db-query-1764329367533.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "ALTER TABLE emailConfig \nADD COLUMN smtpHost VARCHAR(255),\nADD COLUMN smtpPort INT,\nADD COLUMN smtpSecure ENUM('none', 'tls', 'ssl') DEFAULT 'tls',\nADD COLUMN smtpUser VARCHAR(320),\nADD COLUMN smtpPassword TEXT;",
|
||||||
|
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE emailConfig \nADD COLUMN smtpHost VARCHAR(255),\nADD COLUMN smtpPort INT,\nADD COLUMN smtpSecure ENUM('none', 'tls', 'ssl') DEFAULT 'tls',\nADD COLUMN smtpUser VARCHAR(320),\nADD COLUMN smtpPassword TEXT;",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 1627
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { AlertCircle, CheckCircle2, Mail, Send } from "lucide-react";
|
import { AlertCircle, CheckCircle2, Mail, Send, Server } from "lucide-react";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
export default function AdminEmailConfig() {
|
export default function AdminEmailConfig() {
|
||||||
const { data: config, isLoading, refetch } = trpc.emailConfig.get.useQuery();
|
const { data: config, isLoading, refetch } = trpc.emailConfig.get.useQuery();
|
||||||
@@ -16,12 +18,17 @@ export default function AdminEmailConfig() {
|
|||||||
const testEmailMutation = trpc.emailConfig.testEmail.useMutation();
|
const testEmailMutation = trpc.emailConfig.testEmail.useMutation();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
provider: "resend",
|
provider: "resend" as "resend" | "smtp",
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
fromEmail: "",
|
fromEmail: "",
|
||||||
fromName: "Formation Manager Itinova",
|
fromName: "Formation Manager Itinova",
|
||||||
mode: "simulation" as "simulation" | "production",
|
mode: "simulation" as "simulation" | "production",
|
||||||
domainVerified: false,
|
domainVerified: false,
|
||||||
|
smtpHost: "",
|
||||||
|
smtpPort: 587,
|
||||||
|
smtpSecure: "tls" as "none" | "tls" | "ssl",
|
||||||
|
smtpUser: "",
|
||||||
|
smtpPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [testEmail, setTestEmail] = useState("");
|
const [testEmail, setTestEmail] = useState("");
|
||||||
@@ -30,12 +37,17 @@ export default function AdminEmailConfig() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config) {
|
if (config) {
|
||||||
setFormData({
|
setFormData({
|
||||||
provider: config.provider,
|
provider: config.provider as "resend" | "smtp",
|
||||||
apiKey: config.apiKey || "",
|
apiKey: config.apiKey || "",
|
||||||
fromEmail: config.fromEmail,
|
fromEmail: config.fromEmail,
|
||||||
fromName: config.fromName,
|
fromName: config.fromName,
|
||||||
mode: config.mode,
|
mode: config.mode,
|
||||||
domainVerified: config.domainVerified,
|
domainVerified: config.domainVerified,
|
||||||
|
smtpHost: config.smtpHost || "",
|
||||||
|
smtpPort: config.smtpPort || 587,
|
||||||
|
smtpSecure: (config.smtpSecure as "none" | "tls" | "ssl") || "tls",
|
||||||
|
smtpUser: config.smtpUser || "",
|
||||||
|
smtpPassword: config.smtpPassword || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
@@ -150,49 +162,179 @@ export default function AdminEmailConfig() {
|
|||||||
{/* Formulaire de configuration */}
|
{/* Formulaire de configuration */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Configuration Resend</CardTitle>
|
<CardTitle>Configuration de l'envoi d'emails</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configurez votre compte Resend pour envoyer de vrais emails
|
Choisissez votre méthode d'envoi : Resend (API) ou SMTP direct
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Sélecteur de provider */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="apiKey">Clé API Resend *</Label>
|
<Label htmlFor="provider">Méthode d'envoi</Label>
|
||||||
<Input
|
<Select
|
||||||
id="apiKey"
|
value={formData.provider}
|
||||||
type="password"
|
onValueChange={(value: "resend" | "smtp") => setFormData({ ...formData, provider: value })}
|
||||||
placeholder="re_..."
|
>
|
||||||
value={formData.apiKey}
|
<SelectTrigger>
|
||||||
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
<SelectValue />
|
||||||
/>
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="resend">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Mail className="h-4 w-4" />
|
||||||
|
Resend (API)
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="smtp">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
SMTP Direct
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Obtenez votre clé API sur{" "}
|
{formData.provider === "resend"
|
||||||
<a
|
? "Service d'envoi d'emails via API (recommandé)"
|
||||||
href="https://resend.com/api-keys"
|
: "Connexion directe à un serveur SMTP (Gmail, Outlook, etc.)"}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
resend.com/api-keys
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Configuration Resend */}
|
||||||
<Label htmlFor="fromEmail">Email expéditeur *</Label>
|
{formData.provider === "resend" && (
|
||||||
<Input
|
<>
|
||||||
id="fromEmail"
|
<div className="space-y-2">
|
||||||
type="email"
|
<Label htmlFor="apiKey">Clé API Resend *</Label>
|
||||||
placeholder="noreply@votredomaine.com"
|
<Input
|
||||||
value={formData.fromEmail}
|
id="apiKey"
|
||||||
onChange={(e) => setFormData({ ...formData, fromEmail: e.target.value })}
|
type="password"
|
||||||
required
|
placeholder="re_..."
|
||||||
/>
|
value={formData.apiKey}
|
||||||
<p className="text-sm text-muted-foreground">
|
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
||||||
L'adresse email doit être vérifiée dans votre compte Resend
|
/>
|
||||||
</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
</div>
|
Obtenez votre clé API sur{" "}
|
||||||
|
<a
|
||||||
|
href="https://resend.com/api-keys"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
resend.com/api-keys
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="fromEmail">Email expéditeur *</Label>
|
||||||
|
<Input
|
||||||
|
id="fromEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="noreply@votredomaine.com"
|
||||||
|
value={formData.fromEmail}
|
||||||
|
onChange={(e) => setFormData({ ...formData, fromEmail: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
L'adresse email doit être vérifiée dans votre compte Resend
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Configuration SMTP */}
|
||||||
|
{formData.provider === "smtp" && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="smtpHost">Hôte SMTP *</Label>
|
||||||
|
<Input
|
||||||
|
id="smtpHost"
|
||||||
|
type="text"
|
||||||
|
placeholder="smtp.gmail.com"
|
||||||
|
value={formData.smtpHost}
|
||||||
|
onChange={(e) => setFormData({ ...formData, smtpHost: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="smtpPort">Port *</Label>
|
||||||
|
<Input
|
||||||
|
id="smtpPort"
|
||||||
|
type="number"
|
||||||
|
placeholder="587"
|
||||||
|
value={formData.smtpPort}
|
||||||
|
onChange={(e) => setFormData({ ...formData, smtpPort: parseInt(e.target.value) || 587 })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="smtpSecure">Sécurité</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.smtpSecure}
|
||||||
|
onValueChange={(value: "none" | "tls" | "ssl") => setFormData({ ...formData, smtpSecure: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="tls">TLS/STARTTLS (port 587)</SelectItem>
|
||||||
|
<SelectItem value="ssl">SSL (port 465)</SelectItem>
|
||||||
|
<SelectItem value="none">Aucune (port 25)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="smtpUser">Nom d'utilisateur *</Label>
|
||||||
|
<Input
|
||||||
|
id="smtpUser"
|
||||||
|
type="text"
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
value={formData.smtpUser}
|
||||||
|
onChange={(e) => setFormData({ ...formData, smtpUser: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="smtpPassword">Mot de passe *</Label>
|
||||||
|
<Input
|
||||||
|
id="smtpPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Mot de passe ou mot de passe d'application"
|
||||||
|
value={formData.smtpPassword}
|
||||||
|
onChange={(e) => setFormData({ ...formData, smtpPassword: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Pour Gmail, utilisez un{" "}
|
||||||
|
<a
|
||||||
|
href="https://support.google.com/accounts/answer/185833"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
mot de passe d'application
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="fromEmail">Email expéditeur *</Label>
|
||||||
|
<Input
|
||||||
|
id="fromEmail"
|
||||||
|
type="email"
|
||||||
|
placeholder="noreply@votredomaine.com"
|
||||||
|
value={formData.fromEmail}
|
||||||
|
onChange={(e) => setFormData({ ...formData, fromEmail: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="fromName">Nom de l'expéditeur *</Label>
|
<Label htmlFor="fromName">Nom de l'expéditeur *</Label>
|
||||||
|
|||||||
5
drizzle/0016_romantic_sinister_six.sql
Normal file
5
drizzle/0016_romantic_sinister_six.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE `emailConfig` ADD `smtpHost` varchar(255);--> statement-breakpoint
|
||||||
|
ALTER TABLE `emailConfig` ADD `smtpPort` int;--> statement-breakpoint
|
||||||
|
ALTER TABLE `emailConfig` ADD `smtpSecure` enum('none','tls','ssl') DEFAULT 'tls';--> statement-breakpoint
|
||||||
|
ALTER TABLE `emailConfig` ADD `smtpUser` varchar(320);--> statement-breakpoint
|
||||||
|
ALTER TABLE `emailConfig` ADD `smtpPassword` text;
|
||||||
1002
drizzle/meta/0016_snapshot.json
Normal file
1002
drizzle/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,13 @@
|
|||||||
"when": 1764056078540,
|
"when": 1764056078540,
|
||||||
"tag": "0015_young_moonstone",
|
"tag": "0015_young_moonstone",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1764329348207,
|
||||||
|
"tag": "0016_romantic_sinister_six",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -200,6 +200,17 @@ export const emailConfig = mysqlTable("emailConfig", {
|
|||||||
domainVerified: boolean("domainVerified").default(false).notNull(),
|
domainVerified: boolean("domainVerified").default(false).notNull(),
|
||||||
/** Actif ou non */
|
/** Actif ou non */
|
||||||
active: boolean("active").default(true).notNull(),
|
active: boolean("active").default(true).notNull(),
|
||||||
|
// Champs SMTP
|
||||||
|
/** Hôte SMTP (ex: smtp.gmail.com) */
|
||||||
|
smtpHost: varchar("smtpHost", { length: 255 }),
|
||||||
|
/** Port SMTP (ex: 587, 465, 25) */
|
||||||
|
smtpPort: int("smtpPort"),
|
||||||
|
/** Sécurité SMTP (none, tls, ssl) */
|
||||||
|
smtpSecure: mysqlEnum("smtpSecure", ["none", "tls", "ssl"]).default("tls"),
|
||||||
|
/** Nom d'utilisateur SMTP */
|
||||||
|
smtpUser: varchar("smtpUser", { length: 320 }),
|
||||||
|
/** Mot de passe SMTP (chiffré) */
|
||||||
|
smtpPassword: text("smtpPassword"),
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"mysql2": "^3.15.0",
|
"mysql2": "^3.15.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"nodemailer": "^7.0.11",
|
||||||
"openai": "^4.67.0",
|
"openai": "^4.67.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
@@ -98,6 +99,7 @@
|
|||||||
"@types/google.maps": "^3.58.1",
|
"@types/google.maps": "^3.58.1",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.7.0",
|
"@types/node": "^24.7.0",
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/react": "^19.1.16",
|
"@types/react": "^19.1.16",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
|||||||
830
pnpm-lock.yaml
generated
830
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
import { ENV } from "./env";
|
import { ENV } from "./env";
|
||||||
import { notifyOwner } from "./notification";
|
import { notifyOwner } from "./notification";
|
||||||
import { getActiveEmailConfig } from "../db";
|
import { getActiveEmailConfig } from "../db";
|
||||||
|
import { sendViaSMTP } from "./smtpSender";
|
||||||
|
|
||||||
interface EmailParams {
|
interface EmailParams {
|
||||||
to: string;
|
to: string;
|
||||||
@@ -137,20 +138,43 @@ export async function sendEmail(params: EmailParams): Promise<boolean> {
|
|||||||
return await simulateEmail(params);
|
return await simulateEmail(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si mode production, tenter d'envoyer via Resend
|
// Si mode production
|
||||||
if (config.mode === 'production' && config.apiKey) {
|
if (config.mode === 'production') {
|
||||||
const sent = await sendViaResend(params, {
|
// Essayer SMTP en priorité si configuré
|
||||||
apiKey: config.apiKey,
|
if (config.provider === 'smtp' && config.smtpHost && config.smtpPort && config.smtpUser && config.smtpPassword) {
|
||||||
fromEmail: config.fromEmail,
|
const sent = await sendViaSMTP(params, {
|
||||||
fromName: config.fromName,
|
host: config.smtpHost,
|
||||||
});
|
port: config.smtpPort,
|
||||||
|
secure: config.smtpSecure || 'tls',
|
||||||
// Si l'envoi échoue, simuler en fallback
|
user: config.smtpUser,
|
||||||
if (!sent) {
|
password: config.smtpPassword,
|
||||||
return await simulateEmail(params);
|
fromEmail: config.fromEmail,
|
||||||
|
fromName: config.fromName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si l'envoi échoue, simuler en fallback
|
||||||
|
if (!sent) {
|
||||||
|
return await simulateEmail(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// Sinon essayer Resend
|
||||||
|
if (config.provider === 'resend' && config.apiKey) {
|
||||||
|
const sent = await sendViaResend(params, {
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
fromEmail: config.fromEmail,
|
||||||
|
fromName: config.fromName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si l'envoi échoue, simuler en fallback
|
||||||
|
if (!sent) {
|
||||||
|
return await simulateEmail(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: simuler
|
// Fallback: simuler
|
||||||
|
|||||||
103
server/_core/smtpSender.ts
Normal file
103
server/_core/smtpSender.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Service d'envoi d'emails via SMTP avec nodemailer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import type { Transporter } from 'nodemailer';
|
||||||
|
|
||||||
|
interface EmailParams {
|
||||||
|
to: string;
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
filename: string;
|
||||||
|
content: string;
|
||||||
|
contentType: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SMTPConfig {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: 'none' | 'tls' | 'ssl';
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
fromEmail: string;
|
||||||
|
fromName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un transporteur nodemailer à partir de la configuration SMTP
|
||||||
|
*/
|
||||||
|
function createTransporter(config: SMTPConfig): Transporter {
|
||||||
|
const secure = config.secure === 'ssl'; // true pour SSL (port 465), false pour TLS/STARTTLS
|
||||||
|
|
||||||
|
return nodemailer.createTransport({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
secure: secure,
|
||||||
|
auth: {
|
||||||
|
user: config.user,
|
||||||
|
pass: config.password,
|
||||||
|
},
|
||||||
|
// Options supplémentaires pour améliorer la compatibilité
|
||||||
|
tls: {
|
||||||
|
// Ne pas échouer sur les certificats invalides (à utiliser avec précaution en production)
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email via SMTP
|
||||||
|
*/
|
||||||
|
export async function sendViaSMTP(params: EmailParams, config: SMTPConfig): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const transporter = createTransporter(config);
|
||||||
|
|
||||||
|
// Préparer les pièces jointes
|
||||||
|
const attachments = params.attachments?.map(att => ({
|
||||||
|
filename: att.filename,
|
||||||
|
content: att.content,
|
||||||
|
contentType: att.contentType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Envoyer l'email
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: `${config.fromName} <${config.fromEmail}>`,
|
||||||
|
to: params.to,
|
||||||
|
subject: params.subject,
|
||||||
|
html: params.html,
|
||||||
|
attachments: attachments,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Email] Email envoyé via SMTP:', info.messageId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Email] Erreur lors de l\'envoi via SMTP:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teste la connexion SMTP
|
||||||
|
*/
|
||||||
|
export async function testSMTPConnection(config: SMTPConfig): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const transporter = createTransporter(config);
|
||||||
|
|
||||||
|
// Vérifier la connexion
|
||||||
|
await transporter.verify();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Connexion SMTP réussie',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Email] Erreur de connexion SMTP:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erreur de connexion SMTP',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
todo.md
10
todo.md
@@ -728,3 +728,13 @@
|
|||||||
- [x] Corriger update-app.sh pour installer toutes les dépendances (pas seulement --prod)
|
- [x] Corriger update-app.sh pour installer toutes les dépendances (pas seulement --prod)
|
||||||
- [x] Corriger GUIDE_MISE_A_JOUR.md
|
- [x] Corriger GUIDE_MISE_A_JOUR.md
|
||||||
- [x] Recréer le package de déploiement (formation-manager-update-20251127_114306.tar.gz)
|
- [x] Recréer le package de déploiement (formation-manager-update-20251127_114306.tar.gz)
|
||||||
|
|
||||||
|
## Envoi emails via SMTP (sans Resend)
|
||||||
|
|
||||||
|
- [x] Analyser le code d'envoi d'emails actuel
|
||||||
|
- [x] Ajouter les champs SMTP dans le schéma de base de données
|
||||||
|
- [x] Installer nodemailer
|
||||||
|
- [x] Créer une fonction d'envoi SMTP avec nodemailer (smtpSender.ts)
|
||||||
|
- [x] Modifier emailSender.ts pour supporter SMTP
|
||||||
|
- [x] Ajouter l'interface SMTP dans AdminEmailConfig.tsx
|
||||||
|
- [x] Tester l'envoi d'emails via SMTP (interface visible dans Configuration SMTP)
|
||||||
|
|||||||
Reference in New Issue
Block a user