mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
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:
45
Cargo.lock
generated
45
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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" }
|
||||
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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>),
|
||||
}
|
||||
|
||||
257
crates/external_services/src/email/ses.rs
Normal file
257
crates/external_services/src/email/ses.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
1
crates/router/src/services/email.rs
Normal file
1
crates/router/src/services/email.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod types;
|
||||
243
crates/router/src/services/email/assets/invite.html
Normal file
243
crates/router/src/services/email/assets/invite.html
Normal 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>
|
||||
260
crates/router/src/services/email/assets/magic_link.html
Normal file
260
crates/router/src/services/email/assets/magic_link.html
Normal 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>
|
||||
309
crates/router/src/services/email/assets/recon_activated.html
Normal file
309
crates/router/src/services/email/assets/recon_activated.html
Normal 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>
|
||||
229
crates/router/src/services/email/assets/reset.html
Normal file
229
crates/router/src/services/email/assets/reset.html
Normal 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>
|
||||
253
crates/router/src/services/email/assets/verify.html
Normal file
253
crates/router/src/services/email/assets/verify.html
Normal 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>
|
||||
80
crates/router/src/services/email/types.rs
Normal file
80
crates/router/src/services/email/types.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user