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.
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());
}
}
}
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"),
}
}
/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()))
}
}
}