use super::{
MAIL_DELIVERY_PURPOSE, MailClass, MailInput, MailPolicyPlan, RateLimitPolicyError, email_domain, high_risk_domain,
scope, short_hash, source_prefix,
};
fn classify_mail(mail_name: &str) -> Result<MailClass, RateLimitPolicyError> {
match mail_name {
"SignIn" | "SignUp" | "SetPassword" | "ChangePassword" | "VerifyEmail" | "ChangeEmail" | "VerifyChangeEmail"
| "EmailChanged" => Ok(MailClass::Auth),
"MemberInvitation" => Ok(MailClass::WorkspaceInvitation),
"Mention"
| "Comment"
| "CommentMention"
| "MemberAccepted"
| "LinkInvitationReviewRequest"
| "LinkInvitationApprove"
| "LinkInvitationDecline" => Ok(MailClass::CollaborationNotice),
"MemberLeave"
| "MemberRemoved"
| "OwnershipTransferred"
| "OwnershipReceived"
| "TeamWorkspaceUpgraded"
| "TeamBecomeAdmin"
| "TeamBecomeCollaborator"
| "TeamDeleteIn24Hours"
| "TeamDeleteInOneMonth"
| "TeamWorkspaceDeleted"
| "TeamWorkspaceExpireSoon"
| "TeamWorkspaceExpired" => Ok(MailClass::WorkspaceLifecycle),
"TeamLicense" => Ok(MailClass::BillingLicense),
_ => Err(RateLimitPolicyError::UnmappedMailName),
}
}
pub fn plan_mail(input: &MailInput) -> Result<MailPolicyPlan, RateLimitPolicyError> {
let class = classify_mail(&input.mail_name)?;
let recipient_email = input.recipient_email.trim().to_ascii_lowercase();
let recipient_hash = short_hash(&recipient_email);
let domain = email_domain(&recipient_email)?;
let class_name = class.as_str();
let mut scopes = vec![
scope(
format!("mail:recipient:{recipient_hash}:class:{class_name}"),
3600,
20,
1,
),
scope(
format!("mail:recipient_domain:{domain}:class:{class_name}"),
3600,
250,
1,
),
scope("mail:provider_global:default".to_string(), 60, 500, 1),
];
match class {
MailClass::Auth => {
if let Some(prefix) = source_prefix(input.source.as_ref()) {
scopes.push(scope(format!("mail:source_prefix:{prefix}:class:auth"), 3600, 50, 1));
}
}
MailClass::WorkspaceInvitation => {
if let Some(prefix) = source_prefix(input.source.as_ref()) {
scopes.push(scope(
format!("mail:source_prefix_domain:{prefix}:{domain}:class:{class_name}"),
3600,
if high_risk_domain(&domain) { 10 } else { 50 },
1,
));
}
if let Some(subject) = input.metadata.abuse_subject_key.as_deref() {
scopes.push(scope(
format!("mail:abuse_subject:{subject}:class:{class_name}"),
86_400,
0,
1,
));
}
}
MailClass::CollaborationNotice | MailClass::WorkspaceLifecycle => {
if let Some(actor) = input.metadata.actor_user_id.as_deref() {
scopes.push(scope(format!("mail:actor:{actor}:class:{class_name}"), 3600, 200, 1));
}
if let Some(workspace) = input.metadata.workspace_id.as_deref() {
scopes.push(scope(
format!("mail:workspace:{workspace}:class:{class_name}"),
3600,
1000,
1,
));
}
}
MailClass::BillingLicense => {}
}
Ok(MailPolicyPlan {
class,
purpose: MAIL_DELIVERY_PURPOSE,
scopes,
})
}
#[cfg(test)]
#[path = "../tests/rate_limit/mail/tests.rs"]
mod tests;