This guide covers advanced features of the Lettr Rust SDK including attachments, templates, batch sending, error handling, and best practices for production applications.
New to the Rust SDK? Start with the Rust Quickstart to learn the basics first.
Advanced Features
CC and BCC Recipients
Add CC and BCC recipients:
use lettr :: { Lettr , CreateEmailOptions };
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ "primary@example.com" ],
"Team notification" ,
)
. with_cc ([ "cc@example.com" , "cc2@example.com" ])
. with_bcc ([ "bcc@example.com" ])
. with_html ( "<p>This email has CC and BCC recipients.</p>" );
client . emails . send ( email ) . await ? ;
BCC recipients are hidden from all other recipients. They receive the email but their addresses are not visible in the headers.
Reply-To Address
Specify a different reply-to address:
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ "user@example.com" ],
"Support notification" ,
)
. with_reply_to ( "support@yourdomain.com" )
. with_html ( "<p>Click reply to contact our support team.</p>" );
client . emails . send ( email ) . await ? ;
Attachments
Add file attachments to your emails:
use lettr :: { Lettr , CreateEmailOptions , Attachment };
use std :: fs;
// Read file and encode to base64
let file_data = fs :: read ( "invoice.pdf" ) ? ;
let encoded = base64 :: encode ( & file_data );
let email = CreateEmailOptions :: new (
"billing@yourdomain.com" ,
[ "customer@example.com" ],
"Your invoice" ,
)
. with_html ( "<p>Please find your invoice attached.</p>" )
. with_attachments ( vec! [
Attachment {
filename : "invoice.pdf" . to_string (),
content : encoded ,
content_type : "application/pdf" . to_string (),
}
]);
client . emails . send ( email ) . await ? ;
Attach multiple files:
use base64 :: { Engine as _, engine :: general_purpose};
let pdf_data = fs :: read ( "document.pdf" ) ? ;
let img_data = fs :: read ( "logo.png" ) ? ;
let attachments = vec! [
Attachment {
filename : "document.pdf" . to_string (),
content : general_purpose :: STANDARD . encode ( & pdf_data ),
content_type : "application/pdf" . to_string (),
},
Attachment {
filename : "logo.png" . to_string (),
content : general_purpose :: STANDARD . encode ( & img_data ),
content_type : "image/png" . to_string (),
},
];
let email = CreateEmailOptions :: new (
"sender@yourdomain.com" ,
[ "recipient@example.com" ],
"Files attached" ,
)
. with_html ( "<p>See attached files.</p>" )
. with_attachments ( attachments );
client . emails . send ( email ) . await ? ;
Attachments must be base64-encoded. The total size of all attachments should not exceed 10MB. Larger files should be hosted and linked instead.
Templates
Send emails using Lettr-managed templates:
use std :: collections :: HashMap ;
let mut merge_tags = HashMap :: new ();
merge_tags . insert ( "name" . to_string (), "John Doe" . into ());
merge_tags . insert ( "company" . to_string (), "Acme Corp" . into ());
merge_tags . insert ( "verify_url" . to_string (), "https://example.com/verify/abc123" . into ());
merge_tags . insert ( "support_email" . to_string (), "support@yourdomain.com" . into ());
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ "user@example.com" ],
"Welcome" , // Subject can be overridden by template
)
. with_template_id ( "welcome-email" )
. with_merge_tags ( merge_tags );
client . emails . send ( email ) . await ? ;
Templates are managed in the Lettr dashboard . Use merge tags to personalize content without rebuilding HTML in your code.
Add custom email headers:
use std :: collections :: HashMap ;
let mut headers = HashMap :: new ();
headers . insert ( "X-Campaign-ID" . to_string (), "summer-2024" . to_string ());
headers . insert ( "X-Priority" . to_string (), "high" . to_string ());
headers . insert ( "X-Mailer" . to_string (), "Acme Mailer v1.0" . to_string ());
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ "user@example.com" ],
"Custom headers example" ,
)
. with_html ( "<p>This email has custom headers.</p>" )
. with_headers ( headers );
client . emails . send ( email ) . await ? ;
Tracking
Enable open and click tracking:
let email = CreateEmailOptions :: new (
"marketing@yourdomain.com" ,
[ "user@example.com" ],
"Newsletter" ,
)
. with_html ( "<p>Check out <a href='https://example.com'>our website</a>!</p>" )
. with_click_tracking ( true )
. with_open_tracking ( true );
client . emails . send ( email ) . await ? ;
Open tracking works by embedding a transparent pixel image. Click tracking rewrites links to go through Lettr’s tracking domain. Both features respect user privacy and comply with email regulations.
Attach custom metadata for tracking and filtering:
use std :: collections :: HashMap ;
let mut metadata = HashMap :: new ();
metadata . insert ( "user_id" . to_string (), "12345" . to_string ());
metadata . insert ( "order_id" . to_string (), "ORD-98765" . to_string ());
metadata . insert ( "campaign" . to_string (), "abandoned-cart" . to_string ());
metadata . insert ( "environment" . to_string (), "production" . to_string ());
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ "user@example.com" ],
"Order confirmation" ,
)
. with_html ( "<p>Your order has been confirmed.</p>" )
. with_metadata ( metadata );
client . emails . send ( email ) . await ? ;
Metadata is returned in webhook events and can be used to correlate emails with your application data.
Error Handling
The SDK provides idiomatic Rust error handling with the Result type:
use lettr :: { Lettr , CreateEmailOptions , Error };
let email = CreateEmailOptions :: new (
"invalid@unverified-domain.com" ,
[ "user@example.com" ],
"Test" ,
)
. with_html ( "<p>Test</p>" );
match client . emails . send ( email ) . await {
Ok ( response ) => {
println! ( "Email sent: {}" , response . request_id);
}
Err ( Error :: Validation ( err )) => {
// 422 validation errors
eprintln! ( "Validation failed: {}" , err . message);
for ( field , messages ) in err . errors {
eprintln! ( " {}: {:?}" , field , messages );
}
}
Err ( Error :: Authentication ( err )) => {
// 401 authentication errors
eprintln! ( "Authentication failed: {}" , err . message);
}
Err ( Error :: RateLimit ( err )) => {
// 429 rate limit errors
eprintln! ( "Rate limit exceeded: {}" , err . message);
}
Err ( Error :: Api ( err )) => {
// Other API errors (500, 503, etc.)
eprintln! ( "API error: {}" , err . message);
}
Err ( err ) => {
// Network or other errors
eprintln! ( "Request failed: {}" , err );
}
}
Common Error Scenarios
Unverified domain:
Error: Validation error
Message: “The from address domain is not verified”
Solution: Verify your domain in the dashboard
Invalid API key:
Error: Authentication error
Message: “Invalid API key”
Solution: Check your API key is correct and active
Rate limit exceeded:
Error: RateLimit error
Message: “Too many requests”
Solution: Implement exponential backoff retry logic
Batch Sending
Send multiple emails concurrently using Tokio’s join patterns:
use lettr :: { Lettr , CreateEmailOptions };
use tokio;
#[tokio :: main]
async fn main () -> lettr :: Result <()> {
let client = Lettr :: from_env ();
let recipients = vec! [
"user1@example.com" ,
"user2@example.com" ,
"user3@example.com" ,
];
let mut tasks = vec! [];
for recipient in recipients {
let client = client . clone ();
let task = tokio :: spawn ( async move {
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ recipient ],
"Batch notification" ,
)
. with_html ( "<p>This is a batch email.</p>" );
match client . emails . send ( email ) . await {
Ok ( response ) => println! ( "Sent to {}: {}" , recipient , response . request_id),
Err ( e ) => eprintln! ( "Failed to send to {}: {}" , recipient , e ),
}
});
tasks . push ( task );
}
// Wait for all tasks to complete
for task in tasks {
task . await ? ;
}
println! ( "All emails sent" );
Ok (())
}
With Concurrency Limiting
Use a semaphore to limit concurrent requests:
use tokio :: sync :: Semaphore ;
use std :: sync :: Arc ;
async fn send_batch (
client : & Lettr ,
recipients : Vec < & str >,
max_concurrent : usize ,
) -> lettr :: Result <()> {
let semaphore = Arc :: new ( Semaphore :: new ( max_concurrent ));
let mut tasks = vec! [];
for recipient in recipients {
let client = client . clone ();
let permit = semaphore . clone () . acquire_owned () . await ? ;
let task = tokio :: spawn ( async move {
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ recipient ],
"Batch email" ,
)
. with_html ( "<p>Hello!</p>" );
let result = client . emails . send ( email ) . await ;
drop ( permit ); // Release semaphore
result
});
tasks . push ( task );
}
for task in tasks {
task . await ?? ;
}
Ok (())
}
// Usage
let recipients = vec! [ "user1@example.com" , "user2@example.com" , "user3@example.com" ];
send_batch ( & client , recipients , 10 ) . await ? ; // Max 10 concurrent requests
For large batches, consider using a task queue or stream-based approach with futures::stream::StreamExt to manage concurrency and handle failures gracefully.
Async Patterns
Using with Tokio Runtime
The SDK is built on Tokio and works with any Tokio-based application:
use lettr :: Lettr ;
#[tokio :: main]
async fn main () -> lettr :: Result <()> {
let client = Lettr :: from_env ();
// Your async code here
Ok (())
}
Using with Actix Web
Integrate with Actix Web handlers:
use actix_web :: {web, HttpResponse , Result };
use lettr :: { Lettr , CreateEmailOptions };
async fn send_email (
client : web :: Data < Lettr >,
recipient : web :: Path < String >,
) -> Result < HttpResponse > {
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ recipient . as_str ()],
"Hello from Actix" ,
)
. with_html ( "<p>Hello!</p>" );
match client . emails . send ( email ) . await {
Ok ( response ) => Ok ( HttpResponse :: Ok () . json ( response )),
Err ( e ) => Ok ( HttpResponse :: InternalServerError () . body ( e . to_string ())),
}
}
Using with Axum
Integrate with Axum handlers:
use axum :: { extract :: State , Json };
use lettr :: { Lettr , CreateEmailOptions };
async fn send_email (
State ( client ) : State < Lettr >,
recipient : String ,
) -> Result < Json < String >, String > {
let email = CreateEmailOptions :: new (
"notifications@yourdomain.com" ,
[ recipient . as_str ()],
"Hello from Axum" ,
)
. with_html ( "<p>Hello!</p>" );
match client . emails . send ( email ) . await {
Ok ( response ) => Ok ( Json ( response . request_id)),
Err ( e ) => Err ( e . to_string ()),
}
}
Best Practices
Use Environment Variables
Never hardcode API keys. Use environment variables or a secrets manager:
use std :: env;
let api_key = env :: var ( "LETTR_API_KEY" )
. expect ( "LETTR_API_KEY is required" );
let client = Lettr :: new ( & api_key );
Validate Before Sending
Validate email addresses before making API calls:
use validator :: validate_email;
fn is_valid_email ( email : & str ) -> bool {
validate_email ( email )
}
if ! is_valid_email ( & recipient ) {
eprintln! ( "Invalid email address: {}" , recipient );
return Err ( "Invalid email" . into ());
}
Log Request IDs
Always log the request_id from successful sends for tracking and debugging:
use log :: info;
let response = client . emails . send ( email ) . await ? ;
info! ( "Email sent successfully - Request ID: {}, Accepted: {}" ,
response . request_id, response . accepted);
Handle Errors Gracefully
Implement retry logic for transient errors:
use tokio :: time :: {sleep, Duration };
async fn send_with_retry (
client : & Lettr ,
email : CreateEmailOptions ,
max_retries : usize ,
) -> lettr :: Result < String > {
for attempt in 0 ..= max_retries {
match client . emails . send ( email . clone ()) . await {
Ok ( response ) => {
println! ( "Email sent: {}" , response . request_id);
return Ok ( response . request_id);
}
Err ( Error :: Validation ( e )) => {
// Don't retry validation errors
return Err ( Error :: Validation ( e ));
}
Err ( e ) if attempt < max_retries => {
let backoff = Duration :: from_secs (( attempt + 1 ) as u64 );
eprintln! ( "Attempt {} failed, retrying in {:?}: {}" , attempt + 1 , backoff , e );
sleep ( backoff ) . await ;
}
Err ( e ) => return Err ( e ),
}
}
unreachable! ()
}
Clone the Client
The Lettr client is cheap to clone and can be shared across tasks:
let client = Lettr :: from_env ();
// Clone for use in spawned task
let client_clone = client . clone ();
tokio :: spawn ( async move {
// Use client_clone
});
Use Structured Logging
Use the tracing crate for structured logging:
use tracing :: {info, error};
match client . emails . send ( email ) . await {
Ok ( response ) => {
info! (
request_id = % response . request_id,
accepted = response . accepted,
"Email sent successfully"
);
}
Err ( e ) => {
error! ( error = % e , "Failed to send email" );
}
}
Troubleshooting
If you see “The from address domain is not verified”:
Verify your domain in the Lettr dashboard
Ensure the from address uses the verified domain
Wait for DNS propagation (can take up to 48 hours)
See Domain Verification for help
Authentication failed (401 error)
If you see authentication errors:
Check your API key is correct and starts with lttr_
Verify the key is 68 characters total (prefix + 64 hex chars)
Ensure the key hasn’t been revoked in the dashboard
Confirm you’re reading from the correct environment variable
If requests timeout:
Increase the client timeout (default may be too short)
Check your network connectivity and firewall settings
Verify app.lettr.com is reachable
Use a custom reqwest client with longer timeout settings
Compilation or dependency errors
If you see compilation errors:
Run cargo update to update dependencies
Check your Rust version is 1.70 or later: rustc --version
Verify the crate name is correct: lettr
Clear build artifacts: cargo clean
Rate limit exceeded (429 error)
If you’re hitting rate limits:
Implement exponential backoff retry logic (see Best Practices)
Use async batch sending with controlled concurrency via semaphore
Consider upgrading your Lettr plan for higher limits
Spread requests over time instead of bursts
If you encounter Rust-specific type errors:
Ensure your async runtime (Tokio) is properly configured
Check that you’re using .await on async functions
Use .clone() when sharing the client across tasks
Verify all required fields are provided to the builder
What’s Next
API Reference Complete API documentation
Templates Use Lettr-managed templates
Webhooks Track delivery events
Best Practices Email deliverability tips