feat(ses_email): add email services to hyperswitch (#2977)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com>
This commit is contained in:
Narayan Bhat
2023-11-29 16:12:12 +05:30
committed by GitHub
parent 37ab392488
commit 5f5e895f63
18 changed files with 1861 additions and 97 deletions

45
Cargo.lock generated
View File

@ -2366,11 +2366,14 @@ dependencies = [
"aws-config",
"aws-sdk-kms",
"aws-sdk-sesv2",
"aws-sdk-sts",
"aws-smithy-client",
"base64 0.21.4",
"common_utils",
"dyn-clone",
"error-stack",
"hyper",
"hyper-proxy",
"masking",
"once_cell",
"router_env",
@ -2867,6 +2870,30 @@ dependencies = [
"hashbrown 0.14.1",
]
[[package]]
name = "headers"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
dependencies = [
"base64 0.21.4",
"bytes 1.5.0",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -2994,6 +3021,24 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-proxy"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc"
dependencies = [
"bytes 1.5.0",
"futures 0.3.28",
"headers",
"http",
"hyper",
"hyper-tls",
"native-tls",
"tokio 1.32.0",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-rustls"
version = "0.23.2"

View File

@ -322,9 +322,17 @@ region = "" # The AWS region used by the KMS SDK for decrypting data.
# EmailClient configuration. Only applicable when the `email` feature flag is enabled.
[email]
from_email = "notify@example.com" # Sender email
sender_email = "example@example.com" # Sender email
aws_region = "" # AWS region used by AWS SES
base_url = "" # Base url used when adding links that should redirect to self
allowed_unverified_days = 1 # Number of days the api calls ( with jwt token ) can be made without verifying the email
active_email_client = "SES" # The currently active email client
# Configuration for aws ses, applicable when the active email client is SES
[email.aws_ses]
email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails
sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
#tokenization configuration which describe token lifetime and payment method for specific connector
[tokenization]
@ -427,9 +435,6 @@ credit = { currency = "USD" }
debit = { currency = "USD" }
ach = { currency = "USD" }
[pm_filters.stripe]
cashapp = { country = "US", currency = "USD" }
[pm_filters.prophetpay]
card_redirect = { currency = "USD" }

View File

@ -212,9 +212,15 @@ disabled = false
consumer_group = "SCHEDULER_GROUP"
[email]
from_email = "notify@example.com"
sender_email = "example@example.com"
aws_region = ""
base_url = ""
base_url = "http://localhost:8080"
allowed_unverified_days = 1
active_email_client = "SES"
[email.aws_ses]
email_role_arn = ""
sts_role_session_name = ""
[bank_config.eps]
stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }

View File

@ -16,6 +16,7 @@ async-trait = "0.1.68"
aws-config = { version = "0.55.3", optional = true }
aws-sdk-kms = { version = "0.28.0", optional = true }
aws-sdk-sesv2 = "0.28.0"
aws-sdk-sts = "0.28.0"
aws-smithy-client = "0.55.3"
base64 = "0.21.2"
dyn-clone = "1.0.11"
@ -24,6 +25,8 @@ once_cell = "1.18.0"
serde = { version = "1.0.163", features = ["derive"] }
thiserror = "1.0.40"
tokio = "1.28.2"
hyper-proxy = "0.9.1"
hyper = "0.14.26"
# First party crates
common_utils = { version = "0.1.0", path = "../common_utils" }

View File

@ -1,127 +1,163 @@
//! Interactions with the AWS SES SDK
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_sesv2::{
config::Region,
operation::send_email::SendEmailError,
types::{Body, Content, Destination, EmailContent, Message},
Client,
};
use aws_sdk_sesv2::types::Body;
use common_utils::{errors::CustomResult, pii};
use error_stack::{IntoReport, ResultExt};
use masking::PeekInterface;
use serde::Deserialize;
/// Implementation of aws ses client
pub mod ses;
/// Custom Result type alias for Email operations.
pub type EmailResult<T> = CustomResult<T, EmailError>;
/// A trait that defines the methods that must be implemented to send email.
#[async_trait::async_trait]
pub trait EmailClient: Sync + Send + dyn_clone::DynClone {
/// The rich text type of the email client
type RichText;
/// Sends an email to the specified recipient with the given subject and body.
async fn send_email(
&self,
recipient: pii::Email,
subject: String,
body: String,
body: Self::RichText,
proxy_url: Option<&String>,
) -> EmailResult<()>;
/// Convert Stringified HTML to client native rich text format
/// This has to be done because not all clients may format html as the same
fn convert_to_rich_text(
&self,
intermediate_string: IntermediateString,
) -> CustomResult<Self::RichText, EmailError>
where
Self::RichText: Send;
}
/// A super trait which is automatically implemented for all EmailClients
#[async_trait::async_trait]
pub trait EmailService: Sync + Send + dyn_clone::DynClone {
/// Compose and send email using the email data
async fn compose_and_send_email(
&self,
email_data: Box<dyn EmailData + Send>,
proxy_url: Option<&String>,
) -> EmailResult<()>;
}
dyn_clone::clone_trait_object!(EmailClient);
#[async_trait::async_trait]
impl<T> EmailService for T
where
T: EmailClient,
<Self as EmailClient>::RichText: Send,
{
async fn compose_and_send_email(
&self,
email_data: Box<dyn EmailData + Send>,
proxy_url: Option<&String>,
) -> EmailResult<()> {
let email_data = email_data.get_email_data();
let email_data = email_data.await?;
let EmailContents {
subject,
body,
recipient,
} = email_data;
let rich_text_string = self.convert_to_rich_text(body)?;
self.send_email(recipient, subject, rich_text_string, proxy_url)
.await
}
}
/// This is a struct used to create Intermediate String for rich text ( html )
#[derive(Debug)]
pub struct IntermediateString(String);
impl IntermediateString {
/// Create a new Instance of IntermediateString using a string
pub fn new(inner: String) -> Self {
Self(inner)
}
/// Get the inner String
pub fn into_inner(self) -> String {
self.0
}
}
/// Temporary output for the email subject
#[derive(Debug)]
pub struct EmailContents {
/// The subject of email
pub subject: String,
/// This will be the intermediate representation of the the email body in a generic format.
/// The email clients can convert this intermediate representation to their client specific rich text format
pub body: IntermediateString,
/// The email of the recipient to whom the email has to be sent
pub recipient: pii::Email,
}
/// A trait which will contain the logic of generating the email subject and body
#[async_trait::async_trait]
pub trait EmailData {
/// Get the email contents
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError>;
}
dyn_clone::clone_trait_object!(EmailClient<RichText = Body>);
/// List of available email clients to choose from
#[derive(Debug, Clone, Default, Deserialize)]
pub enum AvailableEmailClients {
#[default]
/// AWS ses email client
SES,
}
/// Struct that contains the settings required to construct an EmailClient.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct EmailSettings {
/// Sender email.
pub from_email: String,
/// The AWS region to send SES requests to.
pub aws_region: String,
/// Base-url used when adding links that should redirect to self
pub base_url: String,
/// Number of days for verification of the email
pub allowed_unverified_days: i64,
/// Sender email
pub sender_email: String,
/// Configs related to AWS Simple Email Service
pub aws_ses: Option<ses::SESConfig>,
/// The active email client to use
pub active_email_client: AvailableEmailClients,
}
/// Client for AWS SES operation
#[derive(Debug, Clone)]
pub struct AwsSes {
ses_client: Client,
from_email: String,
}
impl AwsSes {
/// Constructs a new AwsSes client
pub async fn new(conf: &EmailSettings) -> Self {
let region_provider = RegionProviderChain::first_try(Region::new(conf.aws_region.clone()));
let sdk_config = aws_config::from_env().region(region_provider).load().await;
Self {
ses_client: Client::new(&sdk_config),
from_email: conf.from_email.clone(),
}
}
}
#[async_trait::async_trait]
impl EmailClient for AwsSes {
async fn send_email(
&self,
recipient: pii::Email,
subject: String,
body: String,
) -> EmailResult<()> {
self.ses_client
.send_email()
.from_email_address(self.from_email.to_owned())
.destination(
Destination::builder()
.to_addresses(recipient.peek())
.build(),
)
.content(
EmailContent::builder()
.simple(
Message::builder()
.subject(Content::builder().data(subject).build())
.body(
Body::builder()
.text(Content::builder().data(body).charset("UTF-8").build())
.build(),
)
.build(),
)
.build(),
)
.send()
.await
.map_err(AwsSesError::SendingFailure)
.into_report()
.change_context(EmailError::EmailSendingFailure)?;
Ok(())
}
}
#[allow(missing_docs)]
/// Errors that could occur from EmailClient.
#[derive(Debug, thiserror::Error)]
pub enum EmailError {
/// An error occurred when building email client.
#[error("Error building email client")]
ClientBuildingFailure,
/// An error occurred when sending email
#[error("Error sending email to recipient")]
EmailSendingFailure,
/// Failed to generate the email token
#[error("Failed to generate email token")]
TokenGenerationFailure,
/// The expected feature is not implemented
#[error("Feature not implemented")]
NotImplemented,
}
/// Errors that could occur during SES operations.
#[derive(Debug, thiserror::Error)]
pub enum AwsSesError {
/// An error occurred in the SDK while sending email.
#[error("Failed to Send Email {0:?}")]
SendingFailure(aws_smithy_client::SdkError<SendEmailError>),
}

View File

@ -0,0 +1,257 @@
use std::time::{Duration, SystemTime};
use aws_sdk_sesv2::{
config::Region,
operation::send_email::SendEmailError,
types::{Body, Content, Destination, EmailContent, Message},
Client,
};
use aws_sdk_sts::config::Credentials;
use common_utils::{errors::CustomResult, ext_traits::OptionExt, pii};
use error_stack::{report, IntoReport, ResultExt};
use hyper::Uri;
use masking::PeekInterface;
use router_env::logger;
use tokio::sync::OnceCell;
use crate::email::{EmailClient, EmailError, EmailResult, EmailSettings, IntermediateString};
/// Client for AWS SES operation
#[derive(Debug, Clone)]
pub struct AwsSes {
ses_client: OnceCell<Client>,
sender: String,
settings: EmailSettings,
}
/// Struct that contains the AWS ses specific configs required to construct an SES email client
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct SESConfig {
/// The arn of email role
pub email_role_arn: String,
/// The name of sts_session role
pub sts_role_session_name: String,
}
/// Errors that could occur during SES operations.
#[derive(Debug, thiserror::Error)]
pub enum AwsSesError {
/// An error occurred in the SDK while sending email.
#[error("Failed to Send Email {0:?}")]
SendingFailure(aws_smithy_client::SdkError<SendEmailError>),
/// Configuration variable is missing to construct the email client
#[error("Missing configuration variable {0}")]
MissingConfigurationVariable(&'static str),
/// Failed to assume the given STS role
#[error("Failed to STS assume role: Role ARN: {role_arn}, Session name: {session_name}, Region: {region}")]
AssumeRoleFailure {
/// Aws region
region: String,
/// arn of email role
role_arn: String,
/// The name of sts_session role
session_name: String,
},
/// Temporary credentials are missing
#[error("Assumed role does not contain credentials for role user: {0:?}")]
TemporaryCredentialsMissing(String),
/// The proxy Connector cannot be built
#[error("The proxy build cannot be built")]
BuildingProxyConnectorFailed,
}
impl AwsSes {
/// Constructs a new AwsSes client
pub async fn create(conf: &EmailSettings, proxy_url: Option<impl AsRef<str>>) -> Self {
Self {
ses_client: OnceCell::new_with(
Self::create_client(conf, proxy_url)
.await
.map_err(|error| logger::error!(?error, "Failed to initialize SES Client"))
.ok(),
),
sender: conf.sender_email.clone(),
settings: conf.clone(),
}
}
/// A helper function to create ses client
pub async fn create_client(
conf: &EmailSettings,
proxy_url: Option<impl AsRef<str>>,
) -> CustomResult<Client, AwsSesError> {
let sts_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url.as_ref())?
.load()
.await;
let ses_config = conf
.aws_ses
.as_ref()
.get_required_value("aws ses configuration")
.attach_printable("The selected email client is aws ses, but configuration is missing")
.change_context(AwsSesError::MissingConfigurationVariable("aws_ses"))?;
let role = aws_sdk_sts::Client::new(&sts_config)
.assume_role()
.role_arn(&ses_config.email_role_arn)
.role_session_name(&ses_config.sts_role_session_name)
.send()
.await
.into_report()
.change_context(AwsSesError::AssumeRoleFailure {
region: conf.aws_region.to_owned(),
role_arn: ses_config.email_role_arn.to_owned(),
session_name: ses_config.sts_role_session_name.to_owned(),
})?;
let creds = role.credentials().ok_or(
report!(AwsSesError::TemporaryCredentialsMissing(format!(
"{role:?}"
)))
.attach_printable("Credentials object not available"),
)?;
let credentials = Credentials::new(
creds
.access_key_id()
.ok_or(
report!(AwsSesError::TemporaryCredentialsMissing(format!(
"{role:?}"
)))
.attach_printable("Access Key ID not found"),
)?
.to_owned(),
creds
.secret_access_key()
.ok_or(
report!(AwsSesError::TemporaryCredentialsMissing(format!(
"{role:?}"
)))
.attach_printable("Secret Access Key not found"),
)?
.to_owned(),
creds.session_token().map(|s| s.to_owned()),
creds.expiration().and_then(|dt| {
SystemTime::UNIX_EPOCH
.checked_add(Duration::from_nanos(u64::try_from(dt.as_nanos()).ok()?))
}),
"custom_provider",
);
logger::debug!(
"Obtained SES temporary credentials with expiry {:?}",
credentials.expiry()
);
let ses_config = Self::get_shared_config(conf.aws_region.to_owned(), proxy_url)?
.credentials_provider(credentials)
.load()
.await;
Ok(Client::new(&ses_config))
}
fn get_shared_config(
region: String,
proxy_url: Option<impl AsRef<str>>,
) -> CustomResult<aws_config::ConfigLoader, AwsSesError> {
let region_provider = Region::new(region);
let mut config = aws_config::from_env().region(region_provider);
if let Some(proxy_url) = proxy_url {
let proxy_connector = Self::get_proxy_connector(proxy_url)?;
let provider_config = aws_config::provider_config::ProviderConfig::default()
.with_tcp_connector(proxy_connector.clone());
let http_connector =
aws_smithy_client::hyper_ext::Adapter::builder().build(proxy_connector);
config = config
.configure(provider_config)
.http_connector(http_connector);
};
Ok(config)
}
fn get_proxy_connector(
proxy_url: impl AsRef<str>,
) -> CustomResult<hyper_proxy::ProxyConnector<hyper::client::HttpConnector>, AwsSesError> {
let proxy_uri = proxy_url
.as_ref()
.parse::<Uri>()
.into_report()
.attach_printable("Unable to parse the proxy url {proxy_url}")
.change_context(AwsSesError::BuildingProxyConnectorFailed)?;
let proxy = hyper_proxy::Proxy::new(hyper_proxy::Intercept::All, proxy_uri);
hyper_proxy::ProxyConnector::from_proxy(hyper::client::HttpConnector::new(), proxy)
.into_report()
.change_context(AwsSesError::BuildingProxyConnectorFailed)
}
}
#[async_trait::async_trait]
impl EmailClient for AwsSes {
type RichText = Body;
fn convert_to_rich_text(
&self,
intermediate_string: IntermediateString,
) -> CustomResult<Self::RichText, EmailError> {
let email_body = Body::builder()
.html(
Content::builder()
.data(intermediate_string.into_inner())
.charset("UTF-8")
.build(),
)
.build();
Ok(email_body)
}
async fn send_email(
&self,
recipient: pii::Email,
subject: String,
body: Self::RichText,
proxy_url: Option<&String>,
) -> EmailResult<()> {
self.ses_client
.get_or_try_init(|| async {
Self::create_client(&self.settings, proxy_url)
.await
.change_context(EmailError::ClientBuildingFailure)
})
.await?
.send_email()
.from_email_address(self.sender.to_owned())
.destination(
Destination::builder()
.to_addresses(recipient.peek())
.build(),
)
.content(
EmailContent::builder()
.simple(
Message::builder()
.subject(Content::builder().data(subject).build())
.body(body)
.build(),
)
.build(),
)
.send()
.await
.map_err(AwsSesError::SendingFailure)
.into_report()
.change_context(EmailError::EmailSendingFailure)?;
Ok(())
}
}

View File

@ -12,7 +12,7 @@ license.workspace = true
default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"]
s3 = ["dep:aws-sdk-s3", "dep:aws-config"]
kms = ["external_services/kms", "dep:aws-config"]
email = ["external_services/email", "dep:aws-config"]
email = ["external_services/email", "dep:aws-config", "olap"]
basilisk = ["kms"]
stripe = ["dep:serde_qs"]
release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"]

View File

@ -62,4 +62,6 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes
#[cfg(any(feature = "olap", feature = "oltp"))]
pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
#[cfg(feature = "email")]
pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day
pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin";

View File

@ -70,6 +70,28 @@ pub async fn connect_account(
.get_jwt_auth_token(state.clone(), user_role.org_id)
.await?;
#[cfg(feature = "email")]
{
use router_env::logger;
use crate::services::email::types as email_types;
let email_contents = email_types::WelcomeEmail {
recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
settings: state.conf.clone(),
};
let send_email_result = state
.email_client
.compose_and_send_email(
Box::new(email_contents),
state.conf.proxy.https_url.as_ref(),
)
.await;
logger::info!(?send_email_result);
}
return Ok(ApplicationResponse::Json(api::ConnectAccountResponse {
token: Secret::new(jwt_token),
merchant_id: user_role.merchant_id,

View File

@ -2,7 +2,7 @@ use std::sync::Arc;
use actix_web::{web, Scope};
#[cfg(feature = "email")]
use external_services::email::{AwsSes, EmailClient};
use external_services::email::{ses::AwsSes, EmailService};
#[cfg(feature = "kms")]
use external_services::kms::{self, decrypt::KmsDecrypt};
use router_env::tracing_actix_web::RequestId;
@ -45,7 +45,7 @@ pub struct AppState {
pub conf: Arc<settings::Settings>,
pub event_handler: Box<dyn EventHandler>,
#[cfg(feature = "email")]
pub email_client: Arc<dyn EmailClient>,
pub email_client: Arc<dyn EmailService>,
#[cfg(feature = "kms")]
pub kms_secrets: Arc<settings::ActiveKmsSecrets>,
pub api_client: Box<dyn crate::services::ApiClient>,
@ -64,7 +64,7 @@ pub trait AppStateInfo {
fn store(&self) -> Box<dyn StorageInterface>;
fn event_handler(&self) -> Box<dyn EventHandler>;
#[cfg(feature = "email")]
fn email_client(&self) -> Arc<dyn EmailClient>;
fn email_client(&self) -> Arc<dyn EmailService>;
fn add_request_id(&mut self, request_id: RequestId);
fn add_merchant_id(&mut self, merchant_id: Option<String>);
fn add_flow_name(&mut self, flow_name: String);
@ -79,7 +79,7 @@ impl AppStateInfo for AppState {
self.store.to_owned()
}
#[cfg(feature = "email")]
fn email_client(&self) -> Arc<dyn EmailClient> {
fn email_client(&self) -> Arc<dyn EmailService> {
self.email_client.to_owned()
}
fn event_handler(&self) -> Box<dyn EventHandler> {
@ -107,6 +107,15 @@ impl AsRef<Self> for AppState {
}
}
#[cfg(feature = "email")]
pub async fn create_email_client(settings: &settings::Settings) -> impl EmailService {
match settings.email.active_email_client {
external_services::email::AvailableEmailClients::SES => {
AwsSes::create(&settings.email, settings.proxy.https_url.to_owned()).await
}
}
}
impl AppState {
/// # Panics
///
@ -154,7 +163,8 @@ impl AppState {
.expect("Failed while performing KMS decryption");
#[cfg(feature = "email")]
let email_client = Arc::new(AwsSes::new(&conf.email).await);
let email_client = Arc::new(create_email_client(&conf).await);
Self {
flow_name: String::from("default"),
store,

View File

@ -6,6 +6,9 @@ pub mod encryption;
pub mod jwt;
pub mod logger;
#[cfg(feature = "email")]
pub mod email;
#[cfg(feature = "kms")]
use data_models::errors::StorageError;
use data_models::errors::StorageResult;

View File

@ -0,0 +1 @@
pub mod types;

View File

@ -0,0 +1,243 @@
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<title>Welcome to HyperSwitch!</title>
<body style="background-color: #ececec">
<style>
.apple-footer a {{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}}
.apple-email a {{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}}
</style>
<div id="wrapper" style="
background-color: none;
margin: 0 auto;
text-align: center;
width: 60%;
-premailer-height: 200;
">
<table align="center" class="main-table" style="
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #fff;
border: 0;
border-top: 5px solid #0165ef;
margin: 0 auto;
mso-table-lspace: 0;
mso-table-rspace: 0;
padding: 0 40;
text-align: center;
width: 100%;
" bgcolor="#ffffff" cellpadding="0" cellspacing="0">
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="25" width="100%"></td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="50" width="100%"></td>
</tr>
<tr>
<td class="headline" style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 30px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
" align="center">
Welcome to HyperSwitch!
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" width="100%"></td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
Hi {username}<br />
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" width="100%"></td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
You have received this email because your administrator has invited you as a new user on
Hyperswitch.
<br />
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" width="100%"></td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
<b>To get started, click on the button below. </b>
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 100%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" height="20" width="100%"></td>
</tr>
<tr>
<td>
<table align="center">
<tr>
<td align="center" width="160" height="40" class="button" style="
background-color: #0165ef;
border-radius: 2.5px;
display: block;
">
<a href="{link}" style="
width: 100%;
display: inline-block;
text-decoration: none;
font-weight: medium;
color: #fff;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 40px;
">Click here to Join</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
If the link has already expired, you can request a new link from your administrator or reach out to
your internal support for more assistance.<br />
</td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
<tr>
<td class="headline" style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 18px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
" align="center">
Thanks,<br />
Team Hyperswitch
</td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
</table>
</div>
</body>

View File

@ -0,0 +1,260 @@
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<title>Login to Hyperswitch</title>
<body style="background-color: #ececec">
<style>
.apple-footer a {
{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}
}
.apple-email a {
{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}
}
</style>
<div
id="wrapper"
style="
background-color: none;
margin: 0 auto;
text-align: center;
width: 60%;
-premailer-height: 200;
"
>
<table
align="center"
class="main-table"
style="
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #fff;
border: 0;
border-top: 5px solid #0165ef;
margin: 0 auto;
mso-table-lspace: 0;
mso-table-rspace: 0;
padding: 0 40px;
text-align: center;
width: 100%;
"
bgcolor="#ffffff"
cellpadding="0"
cellspacing="0"
>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="25"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="50"
width="100%"
></td>
</tr>
<tr>
<td
class="headline"
style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 30px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
"
align="center"
>
Welcome to Hyperswitch!
<p style="font-size: 18px">Dear {user_name},</p>
<span style="font-size: 18px"
>We are thrilled to welcome you into our community!
</span>
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
width="100%"
></td>
</tr>
<tr>
<td
class="copy"
style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
"
20px!important;
align="center"
>
<br />
Simply click on the link below, and you'll be granted instant access
to your Hyperswitch account. Note that this link expires in 24 hours
and can only be used once.<br />
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 100%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
height="20"
width="100%"
></td>
</tr>
<tr>
<td>
<table align="center">
<tr>
<td
align="center"
width="160"
height="40"
class="button"
style="
background-color: #0165ef;
border-radius: 2.5px;
display: block;
"
>
<a
href="{link}"
style="
width: 100%;
display: inline-block;
text-decoration: none;
font-weight: medium;
color: #fff;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 40px;
"
>Unlock Hyperswitch</a
>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="headline"
style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 18px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
"
align="center"
>
Thanks,<br />
Team Hyperswitch
</td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
</table>
</div>
</body>

View File

@ -0,0 +1,309 @@
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<title>Access Granted to HyperSwitch Recon Dashboard!</title>
<body style="background-color: #ececec">
<style>
<style>
.apple-footer a {{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}}
.apple-email a {{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}}
</style>
</style>
<div
id="wrapper"
style="
background-color: none;
margin: 0 auto;
text-align: center;
width: 60%;
-premailer-height: 200;
"
>
<table
align="center"
class="main-table"
style="
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #fff;
border: 0;
border-top: 5px solid #0165ef;
margin: 0 auto;
mso-table-lspace: 0;
mso-table-rspace: 0;
padding: 0 40;
text-align: center;
width: 100%;
"
bgcolor="#ffffff"
cellpadding="0"
cellspacing="0"
>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="25"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="50"
width="100%"
></td>
</tr>
<tr>
<td
class="headline"
style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 30px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
"
align="center"
>
Access Granted to HyperSwitch Recon Dashboard!
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
width="100%"
></td>
</tr>
<tr>
<td
class="copy"
style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: left;
line-height: 20px;
margin-top: 20px;
padding: 0;
"
20px!important;
align="left"
>
<br />
Dear {username}<br />
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
width="100%"
></td>
</tr>
<tr>
<td
class="copy"
style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: left;
line-height: 20px;
margin-top: 20px;
padding: 0;
"
20px!important;
align="left"
>
<br />
We are pleased to inform you that your Reconciliation access request
has been approved. As a result, you now have authorized access to the
Recon dashboard, allowing you to test its functionality and experience
its benefits firsthand.
<br />
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
width="100%"
></td>
</tr>
<tr>
<td
class="copy"
style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: left;
line-height: 20px;
margin-top: 20px;
padding: 0;
"
20px!important;
align="left"
>
<br />
<b>To access the Recon dashboard, please follow these steps </b>
</td>
</tr>
<tr>
<td
class="copy"
style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: left;
line-height: 20px;
margin-top: 20px;
padding: 0;
"
20px!important;
align="left"
>
<br />
<ol type="1">
<li>
Visit our website at
<a href="https://app.hyperswitch.io/">Hyperswitch Dashboard</a>.
</li>
<li>Click on the "Login" button.</li>
<li>Enter your login credentials to log in.</li>
<li>
Once logged in, you will have full access to the Recon dashboard,
where you can explore its comprehensive features.
</li>
</ol>
Should you have any inquiries or require any form of assistance,
please do not hesitate to reach out to our team on
<a href="https://hyperswitch-io.slack.com/ssb/redirect">Slack </a>,
and we will be more than willing to assist you promptly. <br /><br />
Wishing you a seamless and successful experience as you explore the
capabilities of Hyperswitch.<br />
</td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="headline"
style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 18px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
"
align="center"
>
Thanks,<br />
Team Hyperswitch
</td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
</table>
</div>
</body>

View File

@ -0,0 +1,229 @@
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<title>Hyperswitch Merchant</title>
<body style="background-color: #ececec">
<style>
.apple-footer a {{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}}
.apple-email a {{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}}
</style>
<div id="wrapper" style="
background-color: none;
margin: 0 auto;
text-align: center;
width: 60%;
-premailer-height: 200;
">
<table align="center" class="main-table" style="
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #fff;
border: 0;
border-top: 5px solid #0165ef;
margin: 0 auto;
mso-table-lspace: 0;
mso-table-rspace: 0;
padding: 0 40;
text-align: center;
width: 100%;
" bgcolor="#ffffff" cellpadding="0" cellspacing="0">
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="25" width="100%"></td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="50" width="100%"></td>
</tr>
<tr>
<td class="headline" style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 30px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
" align="center">
Reset Your Password
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" width="100%"></td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
Hey {username}<br />
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" width="100%"></td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
We have received a request to reset your password associated with
<br />
<span style="font-weight: bold"> username : </span>
{username}<br />
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" width="100%"></td>
</tr>
<tr>
<td class="copy" style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
" 20px!important; align="center">
<br />
Click on the below button to reset your password. <br />
</td>
</tr>
<tr>
<td class="spacer-sm" style="
-premailer-height: 20;
-premailer-width: 100%;
line-height: 10px;
margin: 0 auto;
padding: 0;
" height="20" width="100%"></td>
</tr>
<tr>
<td>
<table align="center">
<tr>
<td align="center" width="160" height="40" class="button" style="
background-color: #0165ef;
border-radius: 2.5px;
display: block;
">
<a href="{link}" style="
width: 100%;
display: inline-block;
text-decoration: none;
font-weight: medium;
color: #fff;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 40px;
">Reset Password</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
<tr>
<td class="headline" style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 18px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
" align="center">
Thanks,<br />
Team Hyperswitch
</td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
<tr>
<td class="spacer-lg" style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
" height="75" width="100%"></td>
</tr>
</table>
</div>
</body>

View File

@ -0,0 +1,253 @@
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<title>Hyperswitch Merchant</title>
<body style="background-color: #ececec">
<style>
.apple-footer a {{
text-decoration: none !important;
color: #999 !important;
border: none !important;
}}
.apple-email a {{
text-decoration: none !important;
color: #448bff !important;
border: none !important;
}}
</style>
<div
id="wrapper"
style="
background-color: none;
margin: 0 auto;
text-align: center;
width: 60%;
-premailer-height: 200;
"
>
<table
align="center"
class="main-table"
style="
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #fff;
border: 0;
border-top: 5px solid #0165ef;
margin: 0 auto;
mso-table-lspace: 0;
mso-table-rspace: 0;
padding: 0 40;
text-align: center;
width: 100%;
"
bgcolor="#ffffff"
cellpadding="0"
cellspacing="0"
>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="25"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="50"
width="100%"
></td>
</tr>
<tr>
<td
class="headline"
style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 30px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
"
align="center"
>
Thanks for signing up!<br /><span style="font-size: 18px"
>We need a confirmation of your email address to complete your
registration.</span
>
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 80%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
width="100%"
></td>
</tr>
<tr>
<td
class="copy"
style="
color: #666;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 20px;
margin-top: 20px;
padding: 0;
"
20px!important;
align="center"
>
<br />
Click below to confirm your email address. <br />
</td>
</tr>
<tr>
<td
class="spacer-sm"
style="
-premailer-height: 20;
-premailer-width: 100%;
line-height: 10px;
margin: 0 auto;
padding: 0;
"
height="20"
width="100%"
></td>
</tr>
<tr>
<td>
<table align="center">
<tr>
<td
align="center"
width="160"
height="40"
class="button"
style="
background-color: #0165ef;
border-radius: 2.5px;
display: block;
"
>
<a
href="{link}"
style="
width: 100%;
display: inline-block;
text-decoration: none;
font-weight: medium;
color: #fff;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 14px;
text-align: center;
line-height: 40px;
"
>Verify Email Now</a
>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="headline"
style="
color: #444;
font-family: Roboto, Helvetica, Arial, san-serif;
font-size: 18px;
font-weight: 100;
line-height: 36px;
margin: 0 auto;
padding: 0;
text-align: center;
"
align="center"
>
Thanks,<br />
Team Hyperswitch
</td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
<tr>
<td
class="spacer-lg"
style="
-premailer-height: 75;
-premailer-width: 100%;
line-height: 30px;
margin: 0 auto;
padding: 0;
"
height="75"
width="100%"
></td>
</tr>
</table>
</div>
</body>

View File

@ -0,0 +1,80 @@
use common_utils::errors::CustomResult;
use error_stack::ResultExt;
use external_services::email::{EmailContents, EmailData, EmailError};
use masking::ExposeInterface;
use crate::{configs, consts};
#[cfg(feature = "olap")]
use crate::{core::errors::UserErrors, services::jwt, types::domain::UserEmail};
pub enum EmailBody {
Verify { link: String },
}
pub mod html {
use crate::services::email::types::EmailBody;
pub fn get_html_body(email_body: EmailBody) -> String {
match email_body {
EmailBody::Verify { link } => {
format!(include_str!("assets/verify.html"), link = link)
}
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct EmailToken {
email: String,
expiration: u64,
}
impl EmailToken {
pub async fn new_token(
email: UserEmail,
settings: &configs::settings::Settings,
) -> CustomResult<String, UserErrors> {
let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS);
let expiration = jwt::generate_exp(expiration_duration)?.as_secs();
let token_payload = Self {
email: email.get_secret().expose(),
expiration,
};
jwt::generate_jwt(&token_payload, settings).await
}
}
pub struct WelcomeEmail {
pub recipient_email: UserEmail,
pub settings: std::sync::Arc<configs::settings::Settings>,
}
pub fn get_email_verification_link(
base_url: impl std::fmt::Display,
token: impl std::fmt::Display,
) -> String {
format!("{base_url}/user/verify_email/?token={token}")
}
/// Currently only HTML is supported
#[async_trait::async_trait]
impl EmailData for WelcomeEmail {
async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
.await
.change_context(EmailError::TokenGenerationFailure)?;
let verify_email_link = get_email_verification_link(&self.settings.server.base_url, token);
let body = html::get_html_body(EmailBody::Verify {
link: verify_email_link,
});
let subject = "Welcome to the Hyperswitch community!".to_string();
Ok(EmailContents {
subject,
body: external_services::email::IntermediateString::new(body),
recipient: self.recipient_email.clone().into_inner(),
})
}
}