formulate 1.2.0

formulate is a standalone server that listens for web form data submissions.
#![allow(clippy::blocks_in_conditions)]
use crate::{
    default_subject_line,
    spam::{check_for_spam_allowlist, check_for_spam_blocklist, check_stop_forum_spam},
    strings::{REJECT_MSG, REJECT_MSG_ALT},
};
use rocket::{form::FromForm, response::status::BadRequest, serde::Deserialize};
use validator::Validate;

/// Form submission fields structure
#[derive(Debug, Deserialize, FromForm, Validate)]
#[serde(crate = "rocket::serde")]
pub struct FormSubmission<'a> {
    #[field(name = uncased("first-name"))]
    #[field(name = uncased("first_name"))]
    #[field(name = uncased("firstname"))]
    #[field(name = uncased("full_name"))]
    #[field(name = uncased("fullname"))]
    #[serde(alias = "first-name")]
    #[serde(alias = "firstname")]
    #[serde(alias = "firstName")]
    #[serde(alias = "fullname")]
    #[serde(alias = "fullName")]
    pub full_name: &'a str,

    #[field(name = uncased("last-name"))]
    #[field(name = uncased("last_name"))]
    #[field(name = uncased("lastname"))]
    #[serde(alias = "last-name")]
    #[serde(alias = "lastname")]
    #[serde(alias = "lastName")]
    pub last_name: Option<&'a str>,

    #[field(name = uncased("company-name"))]
    #[field(name = uncased("company_name"))]
    #[field(name = uncased("companyname"))]
    #[serde(alias = "company-name")]
    #[serde(alias = "companyname")]
    #[serde(alias = "companyName")]
    pub company_name: Option<&'a str>,

    #[validate(email(
        message = "Invalid email address provided. Please check email and try again."
    ))]
    #[field(name = uncased("email"))]
    #[field(name = uncased("e-mail"))]
    #[serde(alias = "e-mail")]
    pub email: &'a str,

    #[field(default = "You have received a new message from")]
    #[serde(default = "default_subject_line")]
    pub subject: &'a str,

    pub message: &'a str,

    #[field(name = uncased("phone-number"))]
    #[field(name = uncased("phone_number"))]
    #[field(name = uncased("phonenumber"))]
    #[serde(alias = "phone-number")]
    #[serde(alias = "phonenumber")]
    #[serde(alias = "phoneNumber")]
    pub phone_number: Option<&'a str>,

    #[field(name = uncased("from_site"))]
    #[field(name = uncased("location"))]
    #[field(name = uncased("website"))]
    #[field(name = uncased("site"))]
    #[serde(alias = "location")]
    #[serde(alias = "site")]
    #[serde(alias = "website")]
    pub from_site: &'a str,

    pub redirect: Option<bool>,
}

impl FormSubmission<'_> {
    /// Employs multiple checks to determine if the form that was submitted is spam.
    pub fn check_if_spam(&self) -> Result<(), BadRequest<String>> {
        check_for_spam_allowlist(self.message, REJECT_MSG_ALT)?;
        check_for_spam_blocklist(self.message, REJECT_MSG)?;
        // forum spam check is the lowest priority because it makes a network request
        // if the others find SPAM first, go with those results.
        check_stop_forum_spam(self.email, REJECT_MSG)
    }
}