Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 27 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
91c41f0
feat: add email client with ses impl
Narayanbhat166 Nov 24, 2023
2793728
Merge branch 'main' into add_email_client
Narayanbhat166 Nov 24, 2023
e14f044
fix: minor fixes
Narayanbhat166 Nov 24, 2023
19fa3fb
fix: typos
Narayanbhat166 Nov 24, 2023
27a7169
chore: run formatter
hyperswitch-bot[bot] Nov 24, 2023
6bff390
chore: update Cargo.lock
hyperswitch-bot[bot] Nov 24, 2023
99413f0
fix: ci checks
Narayanbhat166 Nov 24, 2023
2c3c270
chore: run formatter
hyperswitch-bot[bot] Nov 24, 2023
098b572
chore: address PR comments
Narayanbhat166 Nov 27, 2023
388abd0
chore: address PR comments again
Narayanbhat166 Nov 27, 2023
6c30a5d
chore: minor changes
Narayanbhat166 Nov 27, 2023
fbd85b9
Merge branch 'main' into add_email_client
Narayanbhat166 Nov 27, 2023
6ac6c99
refactor: use amazing trait magic and dyns
Narayanbhat166 Nov 27, 2023
9155d16
chore: add new variables in dev.toml
Narayanbhat166 Nov 27, 2023
fa5090b
Merge branch 'main' into add_email_client
Narayanbhat166 Nov 27, 2023
5bf3417
chore: run formatter
hyperswitch-bot[bot] Nov 27, 2023
4ad44f6
chore: cargo clippy
Narayanbhat166 Nov 27, 2023
0ccbbb9
chore: update Cargo.lock
hyperswitch-bot[bot] Nov 27, 2023
6237671
chore: cargo clippy
Narayanbhat166 Nov 27, 2023
d03a903
Merge branch 'main' into add_email_client
Narayanbhat166 Nov 28, 2023
7e92f8b
chore: rename config variable
Narayanbhat166 Nov 28, 2023
b746f74
chore: PR comments
Narayanbhat166 Nov 28, 2023
4cf6cae
chore: rename variable
Narayanbhat166 Nov 28, 2023
5b8f94b
chore: update example config
Narayanbhat166 Nov 28, 2023
755cfcc
Merge branch 'main' into add_email_client
Gnanasundari24 Nov 28, 2023
cca38a8
Merge branch 'main' into add_email_client
Narayanbhat166 Nov 28, 2023
016e3e7
Merge branch 'main' into add_email_client
Narayanbhat166 Nov 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 11 additions & 6 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]" # Sender email
aws_region = "" # AWS region used by AWS SES
base_url = "" # Base url used when adding links that should redirect to self
sender_email = "[email protected]" # 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]
Expand Down Expand Up @@ -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" }

Expand Down
10 changes: 8 additions & 2 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
3 changes: 3 additions & 0 deletions crates/external_services/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" }
Expand Down
194 changes: 115 additions & 79 deletions crates/external_services/src/email.rs
Original file line number Diff line number Diff line change
@@ -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 {
Narayanbhat166 marked this conversation as resolved.
Show resolved Hide resolved
/// 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,
}

/// Client for AWS SES operation
#[derive(Debug, Clone)]
pub struct AwsSes {
ses_client: Client,
from_email: String,
}
/// Number of days for verification of the email
pub allowed_unverified_days: i64,

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;
/// Sender email
pub sender_email: String,

Self {
ses_client: Client::new(&sdk_config),
from_email: conf.from_email.clone(),
}
}
}
/// Configs related to AWS Simple Email Service
pub aws_ses: Option<ses::SESConfig>,

#[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(())
}
/// The active email client to use
pub active_email_client: AvailableEmailClients,
}

#[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>),
}
Loading
Loading