Courrier
La fonctionnalité Envoi de courriels répond à une question : , comment puis-je envoyer un e-mail sur le réseau, sans me soucier de savoir si le backend est un relais SMTP ou une API cloud ? Un MailerPlugin est un transport : remettez-lui un message entièrement formé, récupérez un identifiant opaque et continuez. Le trait est délibérément neutre vis-à-vis du fournisseur : la même forme couvre un client SMTP soutenu par une lettre parlant à votre propre suffixe et un client aws-sdk-sesv2 parlant à Amazon SES. Ce qu'il fait, et non, c'est le rendu des modèles - c'est le travail du futur Capacité de modèle, et la division maintient le courrier petit, ennuyeux et facile à échanger.
Cette page couvre la surface des traits, le contrat corps d'entrée/sortie, comment les erreurs sont classées afin que les appelants sachent si une nouvelle tentative est sûre et comment les deux plugins de référence — @bext/mailer-smtp et @bext/mailer-ses — exercent la forme.
Le trait#
Chaque plugin Mailer implémente un trait, MailerPlugin, défini dans bext-plugin-api::mailer :
pub trait MailerPlugin: Send + Sync {
fn name(&self) -> &str;
fn send(&self, msg: Message) -> Result<SendOutcome, MailerError>;
fn send_batch(&self, msgs: Vec<Message>) -> Vec<BatchOutcome> {
msgs.into_iter().map(|m| self.send(m)).collect()
}
fn is_healthy(&self) -> bool { true }
}
Chaque méthode est synchrone (correspondant à tous les autres traits de bext-plugin-api), de sorte que la forme se déplace proprement à travers les bacs à sable WASM, QuickJS et nsjail dans lesquels les plugins peuvent s'exécuter. Les backends qui nécessitent des E/S asynchrones (le SDK AWS SES, par exemple) bloquent sur leur propre handle d'exécution à l'intérieur du corps du trait. Le plugin de référence SES montre le modèle : soit il emprunte un tokio Handle à l'hôte, soit possède son propre Runtime, puis appelle handle.block_on(req.send()) depuis send.
Message, Address, Attachment, SendOutcome#
Les types de données pris en charge sont intentionnellement petits et conformes à la RFC 5322 :
pub struct Address {
pub email: String,
pub name: Option<String>,
}
pub struct Attachment {
pub filename: String,
pub content_type: String,
pub bytes: Vec<u8>,
pub content_id: Option<String>,
}
pub struct Message {
pub from: Address,
pub to: Vec<Address>,
pub cc: Vec<Address>,
pub bcc: Vec<Address>,
pub reply_to: Vec<Address>,
pub return_path: Option<String>,
pub subject: String,
pub text: Option<String>,
pub html: Option<String>,
pub attachments: Vec<Attachment>,
pub headers: Vec<(String, String)>,
}
pub struct SendOutcome {
pub id: String,
}
Address est la boîte aux lettres RFC 5322 : un e-mail et un nom d'affichage facultatif. Attachment contient des octets bruts et un Content-ID facultatif pour les images en ligne référencées à partir de HTML via cid:.... Le Message possède tout ce dont un transport a besoin, y compris un return_path en option pour le routage DSN afin que le From: et l'expéditeur de l'enveloppe puissent diverger.
SendOutcome::id est une chaîne opaque attribuée par le backend : un Message-ID SMTP pour l’extension SMTP, ou un MessageId SES pour l’extension SES. Les appelants la traitent comme un identifiant de corrélation pour les journaux, les webhooks et les clés d’idempotence. Le trait ne garantit aucun format, et l’extension SMTP peut la laisser vide si le transport ne fournit pas d’identifiant.
Corps déjà rendus#
Au moins un des champs text ou html doit être renseigné pour chaque Message. Si les deux sont vides, le backend renvoie MailerError::InvalidMessage sans tenter l’envoi ; cette erreur ne doit pas être réessayée.
Le type Message contient text: Option<String> et html: Option<String>. Au moins l’un des deux DOIT être renseigné : un backend recevant les deux vides renvoie MailerError::InvalidMessage sans tenter l’envoi. Il ne contient aucune notion de nom de modèle, de variables ou de mise en page. C’est volontaire : les modèles relèvent d’une fonctionnalité distincte, qui pourra être fournie par Template. Les mêler à Mailer imposerait un moteur inutile à chaque extension de transport. Les appelants peuvent aujourd’hui effectuer le rendu avec MJML, MRML, Handlebars ou un composant JSX, puis transmettre les chaînes finales au service d’envoi.
Pourquoi la modification des en-têtes peut être refusée#
Message::headers est un Vec<(String, String)> pour les en-têtes supplémentaires comme List-Unsubscribe, List-Unsubscribe-Post, In-Reply-To ou References. Les backends PEUVENT rejeter les en-têtes qu'ils possèdent eux-mêmes — Bcc, Date, Message-ID, From, To, Subject — en renvoyant MailerError::InvalidMessage. Le plugin de référence SMTP fait exactement cela, rejetant toute tentative de définition de Message-ID car lettre attribue le sien.
Classement des erreurs#
Chaque échec passe par une énumération :
pub enum MailerError {
InvalidMessage(String),
Unauthorized(String),
Rejected(String),
Transport(String),
RateLimited(String),
Unknown(String),
}
La variante est la chose sur laquelle les appelants se branchent. Le String interne est un détail fourni par le backend pour les journaux – ne l’analysez pas.
| Variante | Signification | Réessayer ? |
|---|---|---|
InvalidMessage |
Le message est mal formé : destinataire manquant, corps vide, en-tête incorrect. | Non, corrigez le message. |
Unauthorized |
Expéditeur non vérifié, SMTP AUTH refusé, identité SES manquante. | Non - corrigez la configuration. |
Rejected |
Le backend a accepté la demande puis refusé : liste de suppression, spam. | Non. |
Transport |
Problème au niveau du réseau : réinitialisation TCP, DNS, réponse 5xx. | Oui, avec recul. |
RateLimited |
Le backend vous a demandé de ralentir : SMTP 421, limitation SES. | Oui, après un certain temps. |
Unknown |
Le backend n'a pas pu classer. Traitez comme Transport. |
Avec prudence. |
Les deux plugins de référence partagent le même esprit de classification mais utilisent des signaux différents. Le plugin SMTP a un chemin classify_smtp qui inspecte la catégorie Error::status().severity plus saisie par la lettre et revient à un classify_generic basé sur un mot-clé pour les erreurs de transport. Le plugin SES mappe directement les variantes typées SendEmailError : MessageRejected → Rejected, LimitExceededException → RateLimited, BadRequestException → InvalidMessage.
Lot comme opération de première classe#
send_batch est distinct car la différence de coût est importante. L'impl par défaut boucle sur send, ce qui est la bonne chose pour SMTP (un message par transaction SMTP est normal). Le plugin de référence SES remplace send_batch pour ventiler les envois en parallèle sur son runtime tokio plutôt qu'en série. Il fait que et non utilisent le SendBulkEmail natif de SES, car ce chemin nécessite un modèle SES prédéfini (une préoccupation qui appartient à la future capacité Template) plutôt que les corps pré-rendus que ce trait comporte.
Le vecteur de retour est un résultat par entrée dans le même ordre. Les appelants corrèlent les échecs partiels par index. Un échec au niveau du transport qui abandonne l'ensemble du lot se reflète dans chaque entrée portant le même type d'erreur.
Les plugins de référence#
@bext/mailer-smtp#
Vit à crates/bext-impls/bext-mailer-smtp. Construit sur lettre 0.11 avec les fonctionnalités builder, smtp-transport, rustls-tls, ring et hostname — aucune dépendance OpenSSL. Le plugin est générique sur le trait Transport : le code de production utilise SmtpTransport::relay(host) (TLS implicite sur le port 465) et les tests utilisent StubTransport donc rien ne touche le fil.
Les éléments non évidents :
- Gestion des enveloppes. Lorsque
return_pathest défini, le plugin pré-construit unlettre::address::Envelope::new(Some(parsed), recipients)et l'attache au constructeur avant.multipart(body). lettre n'expose pas de méthode de créationreturn_pathet vous ne pouvez pas muter l'enveloppe une fois le message généré. - En-têtes bruts. Les en-têtes personnalisés atterrissent via
headers.insert_raw(HeaderValue::new(...))avecHeaderName::new_from_ascii. Le plugin refuse de définir les en-têtes que lettre possède déjà. - Carrosseries alternatives.
text+htmlproduisent ensemble unmultipart/alternative;textouhtmlproduit à lui seul un seulSinglePart. Les pièces jointes enveloppent le corps dansmultipart/mixed. - Accessoires en ligne. Lorsque
content_idest défini, le plugin utiliselettre::message::Attachment::new_inline(cid)pour que HTML<img src="cid:...">fonctionne.
@bext/mailer-ses#
Vit à crates/bext-impls/bext-mailer-ses. Construit sur aws-sdk-sesv2 + aws-config. Le plugin encapsule un aws_sdk_sesv2::Client plus un runtime tokio : construisez-le avec with_client_and_handle(client, handle) lorsque l'hôte exécute déjà un runtime, ou with_client_owned_runtime(client) dans le cas contraire.
Les éléments non évidents :
- Routage simple ou brut. SES possède un chemin de contenu
Simple(corps structuré + en-têtes) et un cheminRaw(blob MIME construit à la main). Le plugin utiliseSimplepour les messages simples – SES gère l'encodage, les en-têtes et les métadonnées de livraison. Il passe àRawlorsque le message contient des pièces jointes en-têtes supplémentaires ou, et construit lui-même le MIME en plusieurs parties (y compris un encodeur base64 en ligne, pour éviter de tirer dans une caisse). - Jeu de configurations. Les appelants peuvent joindre un nom d'ensemble de configuration SES au moment de la construction ; c'est ainsi que vous acheminez les envois via un ensemble de réputation spécifique, une destination d'événement ou un pool IP dédié.
- Test . Les tests utilisent
aws_smithy_mocksavec la fonctionnalitétest-utildeaws-sdk-sesv2activée. Les règles sont créées avecmock!(Client::send_email).then_output(...)ou.then_error(...)et transmises àmock_client!(aws_sdk_sesv2, &[&rule])— le test par lots utilise.repeatedly()afin que la règle ne s'épuise pas.
Configuration#
Les deux plugins de référence lisent leur configuration depuis bext.config.toml :
[mailer]
plugin = "@bext/mailer-smtp"
[mailer.smtp]
relay = "smtp.example.com"
username = "apikey"
password = "{{env.SMTP_PASSWORD}}"
starttls = true
timeout_ms = 10_000
Pour le SSE :
[mailer]
plugin = "@bext/mailer-ses"
[mailer.ses]
region = "eu-west-1"
configuration_set = "transactional"
Les informations d'identification pour le plug-in SES proviennent de la chaîne d'informations d'identification AWS standard (variables d'environnement, IMDS, identité de pod EKS, profils) chargée via aws_config::load_defaults(BehaviorVersion::latest()). Le plugin ne lit ni ne stocke les clés lui-même.
Hors de portée#
- Modèle . Corps pré-rendus uniquement. Une future capacité
Templatese superposera. - Carnet d’adresses/listes de destinataires. Les appelants gèrent les leurs.
- Gestion des rebonds. SES envoie des rebonds à un sujet SNS ; le plugin ne s'abonne pas.
- Webhooks de livraison. Faites correspondre le
SendOutcome::idopaque avec le flux d'événements publié par votre backend.
Le trait est un transport. Tout est plus riche en couches au-dessus.
En rapport#
- Aperçu des capacités — l'échelle de promotion et pourquoi les cinq fondamentaux sont expédiés ensemble.
- Capacité planifiée — lancez un
mailer.send(...)selon un planning (rapport quotidien, e-mails de synthèse). - Capacité de traceur — corrèle le
SendOutcome::idd'un envoi avec une durée de requête sortante. - Intégration WordPress — comment
bext-wpreliewp_mail()à la fonctionnalité Mailer via le SDK. - Construire un plugin — échafaudez un plugin Mailer et déclarez
provides_capabilities = ["mailer"].
Links#
- bibliothèque lettre SMTP — Mailer Rust sous-jacent à
@bext/mailer-smtp - RFC 5322 — Format des messages Internet — les formats de fil
MessageetAddresssont façonnés d'après