affine_core 0.0.5

AFFiNE primitive core.
Documentation
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;