Introduction
Bonjour aujourd'hui on va voir avec vous comment automatiser l'envoi des email via Node.js avec le module nodemailer
. Ca va demander quelques connaissances de bases mais rien d'effrayant.
Pré-requis
- Un mail chez private email (A savoir que si vous achetez un nom de domaine chez Namecheap (jss pas sponso), vous pourrez avoir l'email configuré avec, faudra juste créer votre mailbox et vous aurez plus qu'à l'utiliser comme e-mail professionnel.
Prix des noms de domaines chez Namecheap :
- .com, ou extensions de pays (.fr, .ru, etc...) : ~10 à 70€
- .org, .dev, .net, .store, .tech, .art : ~0.80 à 12 €
- .ai : ~80€
- .inc : ~800€
Le mail pro est gratuit pendant 2 mois puis c'est ~30€ par an (les noms de domaines aussi ont les prix à l'année)
La Vie Façon Pratique
Pour rendre la vie plus facile et pas avoir à toucher au code toutes les 5 minutes on s'faire une mini interface web en local pour gérer tout ça.
Etape 1 : Créer le dossier
Installer Next.js
Créer l'app
npx create-next-app@latest <nom_de_l'app>
Spammez la touche entrée, les réponses proposées par défault au prompt qui va suivre cette commande sont les bonnes dans notre cas.
En gros ça va utiliser TypeScript, ESLint & Tailwind.
Installer les dépendances nécessaires
npm install nodemailer
npm install @types/nodemailer
Etape 2 : Initialisez les variables d'environnement
Créez un ficher .env
à la racine du projet fraichement créé puis ajoutez-y ceci :
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
EMAIL_USER="email@superprofessionnel.dev"
EMAIL_PASS="SuperMotDePasse"
SMTP_HOST="mail.privateemail.com"
SMTP_PORT="587"
SMTP_SECURE="false"
En gros on a configuré notre environnement pour utiliser notre mail comme futur émetteur, et on a fait la config SMTP (Protocol de transfert des mails) pour utiliser le bon port et le bon provider.
Gestion de la configuration des cibles (La Base)
Notre Mini Database Locale
Flemme de payer une base de donnée on va se faire un petit JSON en local et on va le configurer. Pour l'instant on va le mettre dans ce style :
Créez un fichier data/contacts.json
:
{
"sas-johndoe": {
"name": "John",
"lastname": "Doe",
"company": "SAS John Doe",
"profession": "Artiste Peintre",
"sex": "him",
"phone": "+33 6 00 00 00 00",
"email": "john.doe@example.com"
},
"verro-sarl": {
"name": "Verronik",
"lastname": "Van Djik",
"company": "Verro SARL",
"profession": "Menuisière",
"sex": "she",
"phone": "+33 6 00 00 00 00",
"email": "verronik@verro-sarl.com"
}
}
Notre Page de Gestion de ce JSON
Il nous faut un formulaire pour modifier/ajouter/supprimer des entrées à ce JSON comme on le ferait avec une DB en gros. Donc on va créer la page et la route correspondante
La Page : app/contacts/page.tsx
'use client';
import { useState, useEffect } from 'react';
interface Contact {
name: string;
lastname: string;
company: string;
profession: string;
sex: 'him' | 'she';
phone: string;
email: string;
}
interface Contacts {
[key: string]: Contact;
}
export default function ContactsPage() {
const [contacts, setContacts] = useState<Contacts>({});
const [editingContact, setEditingContact] = useState<string | null>(null);
const [newContact, setNewContact] = useState<Contact>({
name: '',
lastname: '',
company: '',
profession: '',
sex: 'him',
phone: '',
email: ''
});
const [newKey, setNewKey] = useState('');
const [editingKey, setEditingKey] = useState('');
useEffect(() => {
fetchContacts();
}, []);
const fetchContacts = async () => {
try {
const response = await fetch('/api/contacts');
const data = await response.json();
setContacts(data);
} catch (error) {
console.error('Erreur lors du chargement des contacts:', error);
}
};
const saveContact = async (key: string, contact: Contact) => {
try {
const response = await fetch('/api/contacts', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key, contact }),
});
if (response.ok) {
fetchContacts();
setEditingContact(null);
}
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
}
};
const deleteContact = async (key: string) => {
try {
const response = await fetch('/api/contacts', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key }),
});
if (response.ok) {
fetchContacts();
}
} catch (error) {
console.error('Erreur lors de la suppression:', error);
}
};
const addContact = async () => {
if (newKey && newContact.name && newContact.email) {
await saveContact(newKey, newContact);
setNewKey('');
setNewContact({
name: '',
lastname: '',
company: '',
profession: '',
sex: 'him',
phone: '',
email: ''
});
}
};
const startEdit = (key: string) => {
setEditingContact(key);
setEditingKey(key);
setNewContact(contacts[key]);
};
const cancelEdit = () => {
setEditingContact(null);
setEditingKey('');
setNewContact({
name: '',
lastname: '',
company: '',
profession: '',
sex: 'him',
phone: '',
email: ''
});
};
const updateContact = async () => {
if (editingContact && newContact.name && newContact.email) {
await saveContact(editingContact, newContact);
setEditingContact(null);
setEditingKey('');
setNewContact({
name: '',
lastname: '',
company: '',
profession: '',
sex: 'him',
phone: '',
email: ''
});
}
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold mb-8 text-gray-900">Gestion des Contacts</h1>
{/* Formulaire d'ajout/modification */}
<div className="bg-white p-6 rounded-lg shadow-md mb-6 border">
<h2 className="text-xl font-semibold mb-4 text-gray-800">
{editingContact ? 'Modifier le contact' : 'Ajouter un contact'}
</h2>
<div className="grid grid-cols-2 gap-4">
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Clé</label>
<input
type="text"
placeholder="Clé (ex: sas-johndoe)"
value={editingContact ? editingKey : newKey}
onChange={(e) => editingContact ? setEditingKey(e.target.value) : setNewKey(e.target.value)}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={!!editingContact}
/>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Prénom</label>
<input
type="text"
placeholder="Prénom"
value={newContact.name}
onChange={(e) => setNewContact({...newContact, name: e.target.value})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Nom</label>
<input
type="text"
placeholder="Nom"
value={newContact.lastname}
onChange={(e) => setNewContact({...newContact, lastname: e.target.value})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Entreprise</label>
<input
type="text"
placeholder="Entreprise"
value={newContact.company}
onChange={(e) => setNewContact({...newContact, company: e.target.value})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Profession</label>
<input
type="text"
placeholder="Profession"
value={newContact.profession}
onChange={(e) => setNewContact({...newContact, profession: e.target.value})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Genre</label>
<select
value={newContact.sex}
onChange={(e) => setNewContact({...newContact, sex: e.target.value as 'him' | 'she'})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="him">Homme</option>
<option value="she">Femme</option>
</select>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>Tél</label>
<input
type="text"
placeholder="Téléphone"
value={newContact.phone}
onChange={(e) => setNewContact({...newContact, phone: e.target.value})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className='flex flex-col gap-2'>
<label className='text-black font-bold text-md'>E-Mail</label>
<input
type="email"
placeholder="Email"
value={newContact.email}
onChange={(e) => setNewContact({...newContact, email: e.target.value})}
className="p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-3 mt-6">
{editingContact ? (
<>
<button
onClick={updateContact}
className="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
Mettre à jour
</button>
<button
onClick={cancelEdit}
className="px-6 py-3 bg-gray-500 text-white font-medium rounded-lg hover:bg-gray-600 transition-colors"
>
Annuler
</button>
</>
) : (
<button
onClick={addContact}
className="px-6 py-3 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors"
>
Ajouter
</button>
)}
</div>
</div>
{/* Liste des contacts */}
<div className="space-y-4">
{Object.entries(contacts).map(([key, contact]) => (
<div key={key} className="bg-white p-6 border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg text-gray-900">{contact.name} {contact.lastname}</h3>
<p className="text-gray-700 font-medium">{contact.company}</p>
<p className="text-gray-600">{contact.profession}</p>
<p className="text-blue-600 font-medium">{contact.email}</p>
<p className="text-gray-600">{contact.phone}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => startEdit(key)}
className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors font-medium"
>
Modifier
</button>
<button
onClick={() => deleteContact(key)}
className="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition-colors font-medium"
>
Supprimer
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
La Route : app/api/contacts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
const contactsFilePath = path.join(process.cwd(), 'data', 'contacts.json');
// Assure-toi que le dossier data existe
if (!fs.existsSync(path.dirname(contactsFilePath))) {
fs.mkdirSync(path.dirname(contactsFilePath), { recursive: true });
}
// Crée le fichier s'il n'existe pas
if (!fs.existsSync(contactsFilePath)) {
fs.writeFileSync(contactsFilePath, '{}');
}
export async function GET() {
try {
const contacts = JSON.parse(fs.readFileSync(contactsFilePath, 'utf8'));
return NextResponse.json(contacts);
} catch (error) {
console.error('Erreur API contacts:', error);
return NextResponse.json({ message: 'Erreur serveur' }, { status: 500 });
}
}
export async function PUT(request: NextRequest) {
try {
const { key, contact } = await request.json();
const contacts = JSON.parse(fs.readFileSync(contactsFilePath, 'utf8'));
contacts[key] = contact;
fs.writeFileSync(contactsFilePath, JSON.stringify(contacts, null, 2));
return NextResponse.json({ message: 'Contact sauvegardé' });
} catch (error) {
console.error('Erreur API contacts:', error);
return NextResponse.json({ message: 'Erreur serveur' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest) {
try {
const { key } = await request.json();
const contacts = JSON.parse(fs.readFileSync(contactsFilePath, 'utf8'));
delete contacts[key];
fs.writeFileSync(contactsFilePath, JSON.stringify(contacts, null, 2));
return NextResponse.json({ message: 'Contact supprimé' });
} catch (error) {
console.error('Erreur API contacts:', error);
return NextResponse.json({ message: 'Erreur serveur' }, { status: 500 });
}
}
Gestion de l'Envoi des Emails
Créer la Page d'Envoi
Il nous faut :
- Le Formulaire de composition du Mail (Nom de l'émetteur, Sujet, Mail)
- Une Preview pour voir à quoi va ressembler le Mail
- Un tableau de sélection de ceux qui vont recevoir le Mail (Un, Plusieurs, ou carrément tout le monde)
- Des Variables Mail:
{name}
: Prénom de la personne (required){lastname}
: Nom de la personne (optional){company}
: Nom de l'entreprise (optional){gender_greeting}
: Cher/Chère selon si le sex est him ou she{gender_title}
: Pareil pour : Monsieur/Madame{gender_u}
: Pareil pour : un/une{gender_l}
: Pareil pour le/la{adapt_q}
: que / qu' selon si le mot d'après contient une voyelle (ou un h)- Bref vous pouvez en ajouter un max vous l'aurez compris.
Voici le code de notre page app/send-emails/page.tsx
'use client';
import { useState, useEffect } from 'react';
interface Contact {
name: string;
lastname: string;
company: string;
profession: string;
sex: 'him' | 'she';
phone: string;
email: string;
}
interface Contacts {
[key: string]: Contact;
}
export default function SendEmailPage() {
const [contacts, setContacts] = useState<Contacts>({});
const [selectedContacts, setSelectedContacts] = useState<string[]>([]);
const [senderName, setSenderName] = useState('');
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [previewContact, setPreviewContact] = useState<string>('');
const [sending, setSending] = useState(false);
useEffect(() => {
fetchContacts();
}, []);
const fetchContacts = async () => {
try {
const response = await fetch('/api/contacts');
const data = await response.json();
setContacts(data);
} catch (error) {
console.error('Erreur lors du chargement des contacts:', error);
}
};
const replaceVariables = (text: string, contact: Contact) => {
const variables = {
'{name}': contact.name,
'{lastname}': contact.lastname,
'{company}': contact.company,
'{profession}': contact.profession,
'{gender_greeting}': contact.sex === 'him' ? 'Cher' : 'Chère',
'{gender_title}': contact.sex === 'him' ? 'Monsieur' : 'Madame',
'{gender_u}': contact.sex === 'him' ? 'un' : 'une',
'{gender_l}': contact.sex === 'him' ? 'le' : 'la',
'{phone}': contact.phone,
'{email}': contact.email
};
let result = text;
Object.entries(variables).forEach(([key, value]) => {
result = result.replace(new RegExp(key, 'g'), value || '');
});
// Gestion de {adapt_q}
result = result.replace(/{adapt_q}/g, (match, offset) => {
const nextWord = result.substring(offset + match.length).trim().split(' ')[0];
if (nextWord && /[aeiouàâäéèêëïîôöùûüh]/i.test(nextWord)) {
return "qu'";
}
return 'que ';
});
return result;
};
const toggleContact = (key: string) => {
setSelectedContacts(prev =>
prev.includes(key)
? prev.filter(k => k !== key)
: [...prev, key]
);
};
const selectAll = () => {
setSelectedContacts(Object.keys(contacts));
};
const deselectAll = () => {
setSelectedContacts([]);
};
const sendEmails = async () => {
if (!senderName || !subject || !message || selectedContacts.length === 0) {
alert('Veuillez remplir tous les champs et sélectionner au moins un contact');
return;
}
setSending(true);
try {
const response = await fetch('/api/send-emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
senderName,
subject,
message,
selectedContacts,
contacts
}),
});
if (response.ok) {
alert('Emails envoyés avec succès !');
setSenderName('');
setSubject('');
setMessage('');
setSelectedContacts([]);
} else {
alert('Erreur lors de l\'envoi des emails');
}
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors de l\'envoi des emails');
} finally {
setSending(false);
}
};
const getPreview = () => {
if (!previewContact || !contacts[previewContact]) return '';
return replaceVariables(message, contacts[previewContact]);
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold mb-8 text-gray-900">Envoyer des Emails</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Formulaire de composition */}
<div className="bg-white p-6 rounded-lg shadow-md border">
<h2 className="text-xl font-semibold mb-6 text-gray-900">Composer l'email</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">Nom de l'émetteur</label>
<input
type="text"
value={senderName}
onChange={(e) => setSenderName(e.target.value)}
className="w-full p-3 border border-gray-300 text-black rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Votre nom"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">Sujet</label>
<input
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg text-black focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Sujet de l'email"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">Message</label>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={10}
className="w-full p-3 border border-gray-300 rounded-lg text-black focus:ring-2 focus:ring-blue-500 focus:border-transparent "
placeholder="Votre message avec variables : {name}, {lastname}, {company}, {gender_greeting}, etc."
/>
</div>
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg">
<h3 className="font-semibold mb-3 text-blue-900">Variables disponibles :</h3>
<div className="text-sm space-y-2">
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{name}'}</code> - Prénom</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{company}'}</code> - Entreprise</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{lastname}'}</code> - Nom</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{profession}'}</code> - Profession</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{gender_greeting}'}</code> - Cher/Chère</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{gender_title}'}</code> - Monsieur/Madame</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{gender_u}'}</code> - un/une</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{gender_l}'}</code> - le/la</div>
<div className='text-black'><code className="bg-blue-100 px-2 py-1 rounded text-blue-800">{'{adapt_q}'}</code> - que/qu'</div>
</div>
</div>
</div>
</div>
{/* Sélection des contacts et prévisualisation */}
<div className="bg-white p-6 rounded-lg shadow-md border">
<div>
<label className="block text-sm font-medium mb-3 text-gray-900">Sélection des contacts</label>
<div className="mb-3 flex gap-2">
<button
onClick={selectAll}
className="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-blue-600 transition-colors font-medium"
>
Tout sélectionner
</button>
<button
onClick={deselectAll}
className="bg-gray-500 text-white px-4 py-2 rounded-lg text-sm hover:bg-gray-600 transition-colors font-medium"
>
Tout désélectionner
</button>
</div>
<div className="max-h-48 overflow-y-auto border border-gray-300 rounded-lg p-3 bg-gray-50">
{Object.entries(contacts).map(([key, contact]) => (
<div key={key} className="flex items-center space-x-3 mb-2 p-2 hover:bg-white rounded transition-colors">
<input
type="checkbox"
checked={selectedContacts.includes(key)}
onChange={() => toggleContact(key)}
className="rounded text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">{contact.name} {contact.lastname} ({contact.email})</span>
</div>
))}
</div>
<p className="text-sm text-blue-600 font-medium mt-3">
{selectedContacts.length} contact(s) sélectionné(s)
</p>
</div>
<div className="mt-6">
<label className="block text-sm font-medium mb-3 text-gray-900">Prévisualisation</label>
<select
value={previewContact}
onChange={(e) => setPreviewContact(e.target.value)}
className="w-full p-3 border border-gray-300 text-black rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent mb-4"
>
<option value="">Sélectionner un contact pour la préview</option>
{Object.entries(contacts).map(([key, contact]) => (
<option key={key} value={key}>
{contact.name} {contact.lastname}
</option>
))}
</select>
{previewContact && (
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="border-b border-gray-200 pb-3 mb-4">
<h4 className="font-semibold text-gray-900">À: {contacts[previewContact]?.email}</h4>
<h4 className="font-semibold text-gray-900">Sujet: {subject}</h4>
</div>
<div
className="prose max-w-none text-sm text-gray-800"
dangerouslySetInnerHTML={{
__html: getPreview().replace(/\n/g, '<br>')
}}
/>
</div>
)}
</div>
</div>
</div>
</div>
<div className="mt-8">
<button
onClick={sendEmails}
disabled={sending}
className="w-full lg:w-auto bg-green-600 text-white px-8 py-4 rounded-lg hover:bg-green-700 disabled:bg-gray-400 font-medium text-lg transition-colors shadow-lg hover:shadow-xl"
>
{sending ? 'Envoi en cours...' : 'Envoyer les emails'}
</button>
</div>
</div>
);
}
Créer la route (Envoi des Mails)
Ici bah ducoup faut pouvoir gérer toute les belles choses qu'on a ajouté sur la page pour les rendre toutes fonctionnelles. Donc voici notre route :
La Route : app/api/send-emails/route.ts
import { NextRequest, NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
interface Contact {
name: string;
lastname: string;
company: string;
profession: string;
sex: 'him' | 'she';
phone: string;
email: string;
}
interface Contacts {
[key: string]: Contact;
}
export async function POST(request: NextRequest) {
const { senderName, subject, message, selectedContacts, contacts }: {
senderName: string;
subject: string;
message: string;
selectedContacts: string[];
contacts: Contacts;
} = await request.json();
// Configuration du transporteur SMTP
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
const replaceVariables = (text: string, contact: Contact) => {
const variables = {
'{name}': contact.name,
'{lastname}': contact.lastname,
'{company}': contact.company,
'{profession}': contact.profession,
'{gender_greeting}': contact.sex === 'him' ? 'Cher' : 'Chère',
'{gender_title}': contact.sex === 'him' ? 'Monsieur' : 'Madame',
'{gender_u}': contact.sex === 'him' ? 'un' : 'une',
'{gender_l}': contact.sex === 'him' ? 'le' : 'la',
'{phone}': contact.phone,
'{email}': contact.email
};
let result = text;
Object.entries(variables).forEach(([key, value]) => {
result = result.replace(new RegExp(key, 'g'), value || '');
});
// Gestion de {adapt_q}
result = result.replace(/{adapt_q}/g, (match, offset) => {
const nextWord = result.substring(offset + match.length).trim().split(' ')[0];
if (nextWord && /^[aeiouàâäéèêëïîôöùûüh]/i.test(nextWord)) {
return "qu'";
}
return 'que ';
});
return result;
};
try {
const promises = selectedContacts.map(async (contactKey) => {
const contact = contacts[contactKey];
if (!contact) return;
const personalizedMessage = replaceVariables(message, contact);
const personalizedSubject = replaceVariables(subject, contact);
const mailOptions = {
from: `"${senderName}" <${process.env.EMAIL_USER}>`,
to: contact.email,
subject: personalizedSubject,
text: personalizedMessage,
html: personalizedMessage.replace(/\n/g, '<br>'),
};
return transporter.sendMail(mailOptions);
});
await Promise.all(promises);
return NextResponse.json({
message: 'Emails envoyés avec succès',
count: selectedContacts.length
});
} catch (error) {
console.error('Erreur lors de l\'envoi des emails:', error);
return NextResponse.json({
message: 'Erreur lors de l\'envoi des emails',
error: error instanceof Error ? error.message : 'Erreur inconnue'
}, { status: 500 });
}
}
Navigation et Layout
Créer un layout avec navigation
Créez app/layout.tsx
:
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import './globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const isActive = (path: string) => {
return pathname === path ? 'bg-blue-600' : '';
};
return (
<html lang="fr">
<body>
<div className="min-h-screen bg-gray-50">
<nav className="bg-blue-500 text-white p-4">
<div className="container mx-auto flex space-x-4">
<Link href="/contacts" className={`px-3 py-2 rounded hover:bg-blue-600 ${isActive('/contacts')}`}>
Contacts
</Link>
<Link href="/send-emails" className={`px-3 py-2 rounded hover:bg-blue-600 ${isActive('/send-email')}`}>
Envoyer Email
</Link>
</div>
</nav>
<main className="py-8">
{children}
</main>
</div>
</body>
</html>
);
}
Démarrage de l'application
Une fois tout configuré, vous pouvez démarrer votre application :
npm run dev
Puis accédez à http://localhost:3000/contacts
pour gérer vos contacts ou http://localhost:3000/send-email
pour envoyer des emails.
Conclusion
Voila vous avez réussi, après là le truc est minimaliste hein c'est pas aws non plus. Mais si vous pouvez faire ça, vous pouvez faire le reste :
- Analytics
- Historique
- Gestion des SMS (article précédent)
- Etc...
Et voilà !