🔧 Programmierung 🕛 vor 2 Monaten 8 Min Lesezeit
0

SMTP outbound as a first-class citizen: send emails without pip install yagmail

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Notifications, password resets, magic links, alerts. Every language solves SMTP outbound with an external library. In Fitz, smtp.send(opts) is a language builtin. Async from day one. Bit-for-bit parity between fitz run and fitz build. No pip install yagmail / npm install nodemailer / cargo add lettre / Maven JavaMail.







The detail that gets forgotten



Any serious application eventually needs to send emails:




  • Notifications when a service goes down (incidents → on-call email).

  • Password reset / magic-link auth (unique link to the user's email).

  • Welcome email after a signup.

  • Daily / weekly digest reports dispatched from a cron job.

  • Alerts when a nightly job fails.



And every language solves this with an external library.



We hit the gap while building fitzwatch (open-source status page written in pure Fitz): the heart of the product alerts on-call when a monitor goes down. Without native SMTP, the only option was webhook → n8n/zapier → email. Two external systems just to send one email.



Today, the 8 sub-blocks of the mini-release are in main. One line — smtp.send({...}).await? — and the email goes out.






The typical Python stack






CODE
pip install yagmail
# or use stdlib smtplib (RFC 5321 by hand)









CODE
# With yagmail (friendly syntax):
import yagmail

yag = yagmail.SMTP("[email protected]", "password")
yag.send("[email protected]", "Subject", "Body text")

# With stdlib smtplib (low-level):
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

msg = MIMEMultipart("alternative")
msg["Subject"] = "Subject"
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg.attach(MIMEText("Body text", "plain"))
msg.attach(MIMEText("<p>Body HTML</p>", "html"))

with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login("[email protected]", "password")
server.send_message(msg)






Two options, both require reading the RFC or learning the yagmail API.






The typical JS/Node stack






CODE
npm install nodemailer









CODE
import nodemailer from "nodemailer"

const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: { user: "[email protected]", pass: "password" },
})

await transporter.sendMail({
from: "[email protected]",
to: "[email protected]",
subject: "Subject",
text: "Body text",
html: "<p>Body HTML</p>",
})






One library, well-designed, but npm install brings ~50 transitive packages to package.json.






The typical Rust stack






CODE
cargo add lettre
cargo add tokio --features full









CODE
use lettre::message::{header::ContentType, Mailbox, MultiPart, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};

let creds = Credentials::new("[email protected]".into(), "password".into());
let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.port(587)
.credentials(creds)
.build();

let email = Message::builder()
.from("[email protected]".parse::<Mailbox>().unwrap())
.to("[email protected]".parse::<Mailbox>().unwrap())
.subject("Subject")
.multipart(MultiPart::alternative()
.singlepart(SinglePart::builder().header(ContentType::TEXT_PLAIN).body("Body text".to_string()))
.singlepart(SinglePart::builder().header(ContentType::TEXT_HTML).body("<p>Body HTML</p>".to_string()))
)
.unwrap();

mailer.send(email).await.unwrap();






Functional, but verbose. 25 lines to send ONE email.






The typical Java stack






CODE
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>






And then the canonical recipe of Properties + Session + MimeMessage + Transport.send(...) that probably adds 40 lines.






The Fitz stack






CODE
let r = smtp.send({
"to": "[email protected]",
"from": "[email protected]",
"subject": "Subject",
"body_text": "Body text",
"body_html": "<p>Body HTML</p>",
}).await?






That's it.





  • smtp is a language builtin. No pip install/npm install/cargo add.


  • body_text + body_html together → automatic multipart/alternative.


  • .await? propagates errors as Result::Err(Str). The compiler enforces handling.

  • Config (host, port, user, password, TLS) is read from env vars at first send. Same convention any app in Kubernetes/Docker expects.



r.delivered, r.message_id, and r.duration_ms come out typed as Bool/Str/Int.






What smtp.send guarantees you






1. Zero external deps



The fitz binary ships with lettre 0.11 statically linked inside. When you fitz build, the result is ONE executable of ~10 MB containing the SMTP client, TLS via rustls, no openssl on the target host.




CODE
ldd ./my-app
# linux-vdso.so.1
# libgcc_s.so.1
# libc.so.6
# (no smtp libs, no openssl)









2. Bit-for-bit parity fitz runfitz build



The interpreter (fitz run) and codegen to Rust (fitz build) emit the same wire SMTP. Same handshake, same SCRAM auth, same Message-ID. Bug in one path = bug in the other. E2E tests validate this on every commit.






3. Async from day one



smtp.send(...) returns Future<Result<SmtpResult>>. Integrates naturally with the rest of the stack:




CODE
@background
async fn send_welcome(email: Str) -> Null {
let _ = smtp.send({
"to": email,
"subject": "Welcome",
"body": "Hi, thanks for joining.",
}).await
return null
}

@post("/signup")
fn signup(input: SignupInput) {
// ... create the user in the DB ...
let _ = spawn(send_welcome(input.email))
return 201 { "id": new_user_id }
}






The handler returns 201 to the client immediately. The email goes out in another tokio task. If the SMTP server takes 2 seconds, the client doesn't care — it already responded 2 seconds ago.



And with @cron you build digests:




CODE
@cron("0 0 9 * * *")  // every day at 09:00
async fn daily_digest() -> Null {
let r = smtp.send({
"to": "[email protected]",
"subject": "Daily digest",
"body_html": "<p>Today: ...</p>",
}).await
match r {
Ok(_) => log.info("digest.sent"),
Err(e) => log.error("digest.failed", error: e),
}
return null
}









4. Result<T> as error model



Transport errors (DNS, auth, TLS, server reject) come as Result::Err(Str) with "smtp: " prefix:




CODE
match smtp.send(opts).await {
Ok(r) => log.info("smtp.delivered", message_id: r.message_id),
Err(e) => {
// The prefix lets you classify:
// "smtp: server rejected mail: ..." → 5xx from server
// "smtp: transient error: ..." → 4xx temporary
// "smtp: client error: ..." → TLS/auth fail
// "smtp: invalid `to` address ..." → parse error
log.warn("smtp.failed", error: e)
}
}






The static checker enforces handling. If your fn returns Result<...>, you can propagate with ?. If not, the checker demands match. Zero runtime exceptions.






5. Magic-link auth in 30 lines



The showcase case of Fitz's first-class web stack: HTTP server-side + auth with jwt.encode + SMTP outbound, all combined in one binary.




CODE
type EmailRequest {
email: Str
}

@post("/auth/magic-link")
async fn magic_link(input: EmailRequest) -> Result<Str> {
let payload = { "email": input.email }
let token = jwt.encode(payload, "secret")
let link = "https://app.example.com/verify?t={token}"
let r = smtp.send({
"to": input.email,
"subject": "Your login link",
"body_text": "Click here:\n{link}\n\nExpires in 5 min.",
"body_html": "<p>Click <a href=\"{link}\">here</a>.</p>",
}).await?
return Ok(r.message_id)
}






No Auth0, no Supabase, no Stripe webhooks. One binary, one port, one command to deploy.






Local setup with MailHog



For dev you don't want to send real emails. ). Exhaustive chapter + 3 runnable examples against MailHog in the every time we close a major mini-release. The next one will cover a concrete feature detected during fitzwatch development.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
6 Quellen
CVE-2022-44255 | TOTOLINK LR350 9.3.5u.6369_B20220309 buffer overflow (EUVD-2022-47204)
2 Quellen
CVE-2026-68426 | Linux Kernel up to 6.18.41/7.1.5/7.2-rc3 xfrm validate_xmit_skb_list use after free (Nessus ID 346426)
1 Quelle
Windows 11 Probleme mit gültiger Domänenanmeldung nach September-Update [Workaround]
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten SMTP outbound as a first-class citizen: send emails without pip install yagmail

Thematisch verwandte Begriffe: SMTP, outbound, firstclass, citizen · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...