Creating an email relay with Rust

I created a mobile app that has a contact form. The focus of this page will be describing how I used Rust and the crate lettre to forward form data from the app to a target email address.



  • I've defined a FormData struct to match the fields present in my mobile app:
  • I've used the crates validator and ammonia to perform validation and sanitization on form field data.

    form fields presented to app user
use validator::Validate;
use serde::Deserialize;
use ammonia::clean;

/// A struct describing expected form data and its validation rules
/// - name: A string describing the contact's name with a minimum length of 
/// 1 and a maximum length of 30
/// - country: A string describing the contact's country with a minimum length of
/// 1 and a maximum length of 30
/// - email: A string describing the contact's email address that must be a valid email
/// - message: A string representation of the contact's message with a minimum length of 1
/// - language: An optional string describing the contact's preferred language
#[derive(Debug, Validate, Deserialize, Clone)]
pub struct FormData {
    #[validate(length(min = 1, max = 30))]
    pub name: String,

    #[validate(length(min = 1, max = 30))]
    pub country: String,

    #[validate(email)]
    pub email: String,

    #[validate(length(min = 1))]
    pub message: String,

    // this is set based upon the app users device configuration
    pub language: Option<String>,
}

/// Sanitize form data by cleaning all fields using the defaults provided by the ammonia crate
impl FormData {
    pub fn sanitize(&mut self) {
        self.name = clean(&self.name).to_string();
        self.country = clean(&self.country).to_string();
        self.email = clean(&self.email).to_string();
        self.message = clean(&self.message).to_string();
        if let Some(language) = &self.language {
            self.language = Some(clean(language).to_string());
        }
    }
}                       
                        




  • My app supports six languages, and I've been asked that the email sent contains a subject that is specific to each language.
  • I've used a match statement to accomplish this.
pub fn create_subject_by_lang(form: FormData) -> String {
    match form
        .language
        .clone()
        .unwrap_or(String::from("UNKNOWN LANGUAGE"))
        .as_str()
    {
        "English" => String::from(format!(
            "Answer to your question in English from {}",
            form.country
        )),
        "Español" => String::from(format!(
            "Respuesta a su pregunta en español desde {}",
            form.country
        )),
        "Français" => String::from(format!(
            "Répondez à votre question en français de {}",
            form.country
        )),
        "Português" => String::from(format!(
            "Responda a sua pergunta em português da {}",
            form.country
        )),
        "Italiano" => String::from(format!(
            "Rispondi alla tua domanda in italiano dal {}",
            form.country
        )),
        "Deutsch" => String::from(format!(
            "Antworten zu Ihrer Frage auf Deutsch von {}",
            form.country
        )),
        "UNKNOWN LANGUAGE" => String::from(format!(
            "Unable to detwermine language, the listener is from {}",
            form.country
        )),
        _ => String::from("Unable to determine language and location"),
    }
}
                    




  • Form data is validated and sanitized as soon as it contacts the endpoint.
  • The form fields are then parsed out of the POST request and used to construct an email using the lettre crate.
  • An appropriate response is sent to the client device, this will be used to show a "snack bar" style notification informing the user of either a success or failure.
  • I've used Actix Web to expose and another crate I developed to rate limit the /contact endpoint to client devices.
#[post("/contact")]
pub async fn relay_message(config: web::Data<Arc<Config>>, mut form: web::Form<FormData>) -> HttpResponse {
    if let Err(errors) = form.validate() {
        let error_messages: Vec<String> = errors
            .field_errors()
            .iter()
            .map(|(field, _)| format!("Invalid input in the {} field", field))
            .collect();

        return HttpResponse::BadRequest().json(error_messages);
    }

    form.sanitize();

    info!(
        "Received contact from {} in {} from email {}",
        form.name, form.country, form.email
    );

    let subject = create_subject_by_lang(form.clone());

    let email = Message::builder()
        .from(form.email.parse::<Mailbox>().expect("Failed to parse sender email from form data"))
        .reply_to(form.email.parse::<Mailbox>().expect("Failed to parse reply-to email from form data"))
        .to(config.forward_address.parse().expect("Failed to parse forward_address provided in Config.toml"))
        .subject(subject)
        .body(format!(
            "Name: {}\n\nLocation: {}\n\nEmail: {}\n\nMessage: {}",
            form.name, form.country, form.email, form.message
        ))
        .expect("Failed to build email message from form data");

    let mailer = SmtpTransport::relay(&config.server)
        .expect("Failed to parse relay server provided in Config.toml")
        .credentials(Credentials::from((
            config.user.clone(),
            config.pwd.clone(),
        )))
        .build();

    match mailer.send(&email) {
        Ok(_) => {
            info!("Message from {} was forwarded", form.email);
            HttpResponse::Ok().body("Message sent successfully")
        }
        Err(e) => {
            error!("FAILED to send message to {}", form.email);
            HttpResponse::InternalServerError()
                .body(format!("Failed to send message: {}", e.to_string()))
        }
    }
}
                        


View the project source code on GitHub

Top Of Page