SMTP
Roadster's SMTP support allows sending emails with virtually any email provider. The caveat with using plain SMTP is you may not be able to use the visual email builders provided by some vendors (e.g. Sendgrid or Customer.io). This means that while you're code will be decoupled from any particular vendor, some additional work will be needed to send "pretty" emails that match your app's design style. However, if you just need to send plain text emails, or are willing to do the additional work to create "pretty" emails, SMTP is a great option for sending emails for your app.
Starting a local SMTP service for testing
There are several SMTP servers that can be run locally for testing. This is another benefit of using SMTP instead of Sendgrid -- a local SMTP instance can be used to easily verify the contents of your emails and your sending logic, while Sendgrid only provides minimal dev/testing support and can't be run locally.
Below are a few options for development SMTP services that can be easily run locally with docker.
Mailpit
docker run -d -p 8025:8025 -p 1025:1025 axllent/mailpit
smtp4dev
docker run -d -p 1080:80 -p 1025:25 rnwood/smtp4dev
maildev
docker run -d -p 1080:1080 -p 1025:1025 maildev/maildev
Configure the SMTP integration
The SMTP connection details can be configured via your app's config files, and via env vars or an
AsyncSource
for
sensitive connection details. Below is a sample config file that can be used to connect to a locally-hosted SMTP
service.
# Note: Hard-coded connection details are used here for demonstration purposes only. In a real application,
# an `AsyncSource` should be used to fetch secrets from an external service, such as AWS or GCS
# secrets manager services.
[email]
from = "no-reply@example.com"
[email.smtp.connection]
# The `smtps` scheme should be used in production
uri = "smtp://localhost:1025"
# Alternatively, provide connection details as individual fields
#host = "smtp.example.com"
#port = 465
#username = "username"
#password = "password"
Sending plaintext emails
The easiest way to send an email is to simply send a plaintext email.
pub struct EmailConfirmationPlainText {
state: AppContext,
}
impl EmailConfirmationPlainText {
pub fn new(state: &AppContext) -> Self {
Self {
state: state.clone(),
}
}
}
#[derive(Debug, TypedBuilder, Serialize, Deserialize)]
pub struct EmailConfirmationPlainTextArgs {
user_id: Uuid,
}
#[async_trait]
impl Worker<EmailConfirmationPlainTextArgs> for EmailConfirmationPlainText {
#[instrument(skip_all)]
async fn perform(&self, args: EmailConfirmationPlainTextArgs) -> sidekiq::Result<()> {
let user = User::find_by_id(&self.state, args.user_id).await?;
send_email(&self.state, &user).await?;
Ok(())
}
}
/// Send the verification email to the user.
async fn send_email(state: &AppContext, user: &User) -> RoadsterResult<()> {
let verify_url = "https://exaple.com?verify=1234";
let body = body(&user.name, verify_url);
let email: MessageBuilder = (&state.config().email).into();
let email = email
.to(Mailbox::from_str(&user.email)?)
.subject("Please confirm your email address")
// Set the content type as plaintext
.header(ContentType::TEXT_PLAIN)
.body(body)?;
state.smtp().send(&email)?;
info!(user=%user.id, "Email confirmation sent");
Ok(())
}
/// Build the plaintext email content.
fn body(name: &str, verify_url: &str) -> String {
format!(
r#"Hello {name},
Please open the below link in your browser to verify your email:
{verify_url}
"#
)
}
Sending html emails with Leptos
In order to send HTML content via SMTP, you can either manually write your HTML, or use something like Leptos. Leptos is a reactive Rust UI framework, but it can also be used as a simple HTML templating system. It may be possible to use other frameworks such as Yew or Dioxus as well.
The below example is the same as the plaintext example, except it formats the email message with HTML using Leptos.
pub struct EmailConfirmationHtml {
state: AppContext,
}
impl EmailConfirmationHtml {
pub fn new(state: &AppContext) -> Self {
Self {
state: state.clone(),
}
}
}
#[derive(Debug, TypedBuilder, Serialize, Deserialize)]
pub struct EmailConfirmationHtmlArgs {
user_id: Uuid,
}
#[async_trait]
impl Worker<EmailConfirmationHtmlArgs> for EmailConfirmationHtml {
#[instrument(skip_all)]
async fn perform(&self, args: EmailConfirmationHtmlArgs) -> sidekiq::Result<()> {
let user = User::find_by_id(&self.state, args.user_id).await?;
send_email(&self.state, &user).await?;
Ok(())
}
}
/// Send the verification email to the user.
async fn send_email(state: &AppContext, user: &User) -> RoadsterResult<()> {
let verify_url = "https://exaple.com?verify=1234";
let body = body(&user.name, verify_url);
let email: MessageBuilder = (&state.config().email).into();
let email = email
.to(Mailbox::from_str(&user.email)?)
.subject("Please confirm your email address")
// Set the content type as html
.header(ContentType::TEXT_HTML)
.body(body.to_html())?;
state.smtp().send(&email)?;
info!(user=%user.id, "Email confirmation sent");
Ok(())
}
/// Build the email body as HTML using Leptos.
fn body(name: &str, verify_url: &str) -> impl IntoView {
view! {
<div>
<p>"Hello "{name}","</p>
<p>"Please click the link below to confirm your email address."</p>
<a href=verify_url rel="noopener noreferrer">
"Verify email"
</a>
</div>
}
}