diff --git a/backend/src/config.rs b/backend/src/config.rs index 80f67b1a9..bd8066ca4 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use crate::storage::commercial_license_trial::CommercialLicenseTrialStorage; use crate::storage::{postgres::PostgresStorage, Storage}; use crate::worker::do_work::TaskWebhook; use crate::worker::setup_rabbit_mq; @@ -112,6 +113,15 @@ impl BackendConfig { .with_context(|| format!("Connecting to postgres DB {}", config.db_url))?; self.storages.push(Arc::new(storage)); } + StorageConfig::CommercialLicenseTrial(config) => { + let storage = + CommercialLicenseTrialStorage::new(&config.db_url, config.extra.clone()) + .await + .with_context(|| { + format!("Connecting to postgres DB {}", config.db_url) + })?; + self.storages.push(Arc::new(storage)); + } } } @@ -214,6 +224,10 @@ impl ThrottleConfig { pub enum StorageConfig { /// Store the email verification results in the Postgres database. Postgres(PostgresConfig), + /// Store the email verification results in Reacher's DB. This storage + /// method is baked-in into the software for users of the Commercial + /// License trial. + CommercialLicenseTrial(PostgresConfig), } #[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] diff --git a/backend/src/storage/commercial_license_trial.rs b/backend/src/storage/commercial_license_trial.rs new file mode 100644 index 000000000..71c3f611d --- /dev/null +++ b/backend/src/storage/commercial_license_trial.rs @@ -0,0 +1,144 @@ +// Reacher - Email Verification +// Copyright (C) 2018-2023 Reacher + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use super::error::StorageError; +use super::postgres::PostgresStorage; +use super::Storage; +use crate::worker::do_work::{CheckEmailJobId, CheckEmailTask, TaskError}; +use async_trait::async_trait; +use check_if_email_exists::{redact, CheckEmailOutput, LOG_TARGET}; +use serde_json::Value; +use tracing::debug; + +/// Storage that's baked in the software for users of the Commercial License +/// trial. It's really just a wrapper around the PostgresStorage, where we +/// redact all sensitive data such as the email address. +#[derive(Debug)] +pub struct CommercialLicenseTrialStorage { + postgres_storage: PostgresStorage, +} + +impl CommercialLicenseTrialStorage { + pub async fn new(db_url: &str, extra: Option) -> Result { + let postgres_storage = PostgresStorage::new(db_url, extra).await?; + Ok(Self { postgres_storage }) + } +} + +#[async_trait] +impl Storage for CommercialLicenseTrialStorage { + async fn store( + &self, + task: &CheckEmailTask, + worker_output: &Result, + extra: Option, + ) -> Result<(), StorageError> { + let mut payload_json = serde_json::to_value(task)?; + if let Ok(output) = worker_output { + redact_across_json(&mut payload_json, &output.syntax.username); + } + + match worker_output { + Ok(output) => { + let mut output_json = serde_json::to_value(output)?; + redact_across_json(&mut output_json, &output.syntax.username); + + sqlx::query!( + r#" + INSERT INTO v1_task_result (payload, job_id, extra, result) + VALUES ($1, $2, $3, $4) + RETURNING id + "#, + payload_json, + match task.job_id { + CheckEmailJobId::Bulk(job_id) => Some(job_id), + CheckEmailJobId::SingleShot => None, + }, + extra, + output_json, + ) + .fetch_one(&self.postgres_storage.pg_pool) + .await?; + } + Err(err) => { + sqlx::query!( + r#" + INSERT INTO v1_task_result (payload, job_id, extra, error) + VALUES ($1, $2, $3, $4) + RETURNING id + "#, + payload_json, + match task.job_id { + CheckEmailJobId::Bulk(job_id) => Some(job_id), + CheckEmailJobId::SingleShot => None, + }, + extra, + err.to_string(), + ) + .fetch_one(&self.postgres_storage.pg_pool) + .await?; + } + } + + debug!(target: LOG_TARGET, email=?task.input.to_email, "Wrote to DB"); + + Ok(()) + } + + fn get_extra(&self) -> Option { + self.postgres_storage.get_extra() + } +} + +/// Redact all sensitive data by recursively traversing the JSON object. +fn redact_across_json(value: &mut Value, username: &str) { + match value { + Value::String(s) => *s = redact(s, username), + Value::Array(arr) => { + for item in arr { + redact_across_json(item, username); + } + } + Value::Object(obj) => { + for (_, v) in obj { + redact_across_json(v, username); + } + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use check_if_email_exists::{check_email, CheckEmailInputBuilder}; + + #[tokio::test] + async fn should_redact_across_json() { + let input = CheckEmailInputBuilder::default() + // Checking this email will make a MX record check, but hopefully + // it won't resolve (since I typed it randomly), meaning that the + // SMTP check will be skipped. + .to_email("someone@adlkfjaklsdjfldksjfderlqkjeqwr.com".into()) + .build() + .unwrap(); + let output = check_email(&input).await; + let mut output_json = serde_json::to_value(&output).unwrap(); + redact_across_json(&mut output_json, &output.syntax.username); + + assert!(!output_json.to_string().contains("someone")); + } +} diff --git a/backend/src/storage/mod.rs b/backend/src/storage/mod.rs index 7d6249f05..fcd1afcea 100644 --- a/backend/src/storage/mod.rs +++ b/backend/src/storage/mod.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +pub mod commercial_license_trial; pub mod error; pub mod postgres; diff --git a/core/src/util/sentry.rs b/core/src/util/sentry.rs index 2314e5f6d..427a99b1e 100644 --- a/core/src/util/sentry.rs +++ b/core/src/util/sentry.rs @@ -92,7 +92,7 @@ fn error(err: SentryError, result: &CheckEmailOutput, backend_name: &str) { /// Function to replace all usernames from email, and replace them with /// `***@domain.com` for privacy reasons. -fn redact(input: &str, username: &str) -> String { +pub fn redact(input: &str, username: &str) -> String { input.replace(username, "***") }