reformulate 0.4.0

reformulate is a standalone server that listens for web form data submissions.
use actix_web::error;
use awc::Client;
use once_cell::sync::Lazy;
use serde::Deserialize;

type SpamCheckResult = Result<(), error::Error>;

/// Configuration structure for spam blocking features
#[derive(Deserialize)]
struct SpamConfig {
    /// List of domain names. The presence of *any* of these in the form submission's message body is used
    /// as a positive signal for the message being spam.
    blocklist: Option<String>,
    /// Whether to enable spam blocking. If true, the StopForumSpam API will be used to check if the contact
    /// address submitted with the form.
    blocking: Option<bool>,
}

// fspamlist also has API, but it requires API key.
#[derive(Deserialize)]
struct SFSEmailResponse {
    /// email address being queried for.
    value: String,
    /// Whether or not email appears in the SFS database
    appears: usize,
    // /// Number of times email appears in the SFS database
    // frequency: usize,
    // lastseen: Option<String>,
    // /// Statistically calculated score, based on the last seen date and the number of sightings
    // confidence: Option<f64>,
}

/// Configuration structure for stop forum spam
#[derive(Deserialize)]
struct StopForumSpamJsonResponse {
    success: u8,
    email: SFSEmailResponse,
    error: Option<String>,
}

static SPAM_CONFIG: Lazy<SpamConfig> = Lazy::new(|| SpamConfig {
    blocking: match std::env::var("FORMULATE_SPAM_BLOCKING") {
        Ok(config) => Some(config.to_lowercase().trim() == "true"),
        Err(_) => None,
    },
    blocklist: match std::env::var("FORMULATE_SPAM_BLOCKLIST") {
        Ok(list) => Some(list),
        Err(_) => None,
    },
});

/// Checks if email being used has a high confidence of being a form spammer.
/// If Result is OK then API check failed, or email not found in spam database.
/// If Result is an error, a positive match was found against the spam database
/// for this submitted email.
pub async fn check_stop_forum_spam(form_email: &str, error_msg: &str) -> SpamCheckResult {
    if SPAM_CONFIG.blocking.is_none() {
        return Ok(());
    }

    if SPAM_CONFIG.blocking.unwrap() {
        let sfs_api_url = format!("http://api.stopforumspam.org/api?email={form_email}&json");
        let result = Client::new()
            .post(&sfs_api_url)
            .insert_header(("Content-Type", "application/x-www-form-urlencoded"))
            .send()
            .await;

        if result.is_err() {
            // If there was an error communicating with the API we don't want to throw
            // an error (an error means a match was found in the spam db).
            return Ok(());
        }
        let result = result.unwrap().json::<StopForumSpamJsonResponse>().await;

        if result.is_err() {
            // If there was an error obtaining the API response we don't want to throw
            // an error (an error means a match was found in the spam db)
            return Ok(());
        }
        let result = result.unwrap();

        if result.error.is_none()
            && result.success == 1
            && result.email.appears > 0
            && result.email.value.eq(form_email)
        {
            Err(error::ErrorBadRequest(error_msg.to_string()))
        } else {
            Ok(())
        }
    } else {
        Ok(())
    }
}

/// Checks a (form submission) message for matches against a list of domains. If matches
/// are found, an error is returned which ultimately ends form processing. If no matches
/// are found or no list is available, the normal control flow continues.
pub fn check_for_spam(form_message: &str, error_msg: &str) -> SpamCheckResult {
    if SPAM_CONFIG.blocklist.is_some() {
        let blocklist = SPAM_CONFIG.blocklist.as_ref().unwrap();
        let domain_list = blocklist.trim().split(',').collect::<Vec<_>>();
        let looks_like_spam = domain_list.iter().any(|url| form_message.contains(url));

        if looks_like_spam {
            return Err(error::ErrorBadRequest(error_msg.to_string()));
        }
    }

    Ok(())
}