feat: rebuild email platform and operations console
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
use serde::Deserialize;
|
||||
use std::fmt;
|
||||
use validator::Validate;
|
||||
|
||||
/// 注册请求
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct RegisterRequest {
|
||||
#[validate(email)]
|
||||
pub email: String,
|
||||
#[validate(length(min = 8, max = 128))]
|
||||
pub password: String,
|
||||
/// 邀请码(配置 invite_code 为空时不校验)
|
||||
pub invite_code: Option<String>,
|
||||
}
|
||||
|
||||
// 实现 Debug trait,对密码进行脱敏
|
||||
impl fmt::Debug for RegisterRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"RegisterRequest {{ email: {}, password: *** }}",
|
||||
self.email
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录请求
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct LoginRequest {
|
||||
#[validate(email)]
|
||||
pub email: String,
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// 实现 Debug trait
|
||||
impl fmt::Debug for LoginRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "LoginRequest {{ email: {}, password: *** }}", self.email)
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除用户请求
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteUserRequest {
|
||||
pub user_id: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// 实现 Debug trait
|
||||
impl fmt::Debug for DeleteUserRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"DeleteUserRequest {{ user_id: {}, password: *** }}",
|
||||
self.user_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新令牌请求
|
||||
#[derive(Deserialize)]
|
||||
pub struct RefreshRequest {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
// RefreshRequest 的 refresh_token 是敏感字段,需要脱敏
|
||||
impl fmt::Debug for RefreshRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "RefreshRequest {{ refresh_token: *** }}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
/// 创建临时邮箱请求
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct CreateMailboxRequest {
|
||||
/// 自定义本地部分(不传则随机生成)
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
pub local_part: Option<String>,
|
||||
/// 自定义域名(不传则用配置的第一个 local_domain)
|
||||
#[validate(length(min = 1, max = 255))]
|
||||
pub domain: Option<String>,
|
||||
/// 用途备注
|
||||
#[validate(length(max = 128))]
|
||||
pub note: Option<String>,
|
||||
/// 自定义有效期(小时),不传则用配置默认
|
||||
pub ttl_hours: Option<i64>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CreateMailboxRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CreateMailboxRequest")
|
||||
.field("local_part", &self.local_part)
|
||||
.field("domain", &self.domain)
|
||||
.field("note", &self.note)
|
||||
.field("ttl_hours", &self.ttl_hours)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod mailbox;
|
||||
pub mod user;
|
||||
@@ -0,0 +1,12 @@
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct UpdateProfileRequest {
|
||||
#[validate(length(max = 80))]
|
||||
pub display_name: Option<String>,
|
||||
#[validate(url)]
|
||||
pub avatar_url: Option<String>,
|
||||
#[validate(length(max = 500))]
|
||||
pub bio: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 滥用规则触发事件
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "abuse_events")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub rule_id: i64,
|
||||
/// 触发的 domain / ip
|
||||
pub target: String,
|
||||
/// JSON 详情
|
||||
pub detail: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 滥用检测规则
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "abuse_rules")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
/// domain_emails_10min | ip_connects_1min | ...
|
||||
pub metric: String,
|
||||
pub window_seconds: i64,
|
||||
pub threshold: i64,
|
||||
/// auto_block_sender | auto_block_ip | alert | quarantine
|
||||
pub action: String,
|
||||
pub block_ttl_seconds: i64,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,44 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 审计日志(SMTP 事件 + 管理操作)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "audit_logs")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
/// smtp_connect | smtp_rcpt | smtp_accept | smtp_reject | admin_* | ...
|
||||
pub event_type: String,
|
||||
pub source_ip: Option<String>,
|
||||
pub helo: Option<String>,
|
||||
pub mail_from: Option<String>,
|
||||
pub rcpt_to: Option<String>,
|
||||
/// accept | defer | reject | quarantine | ...
|
||||
pub action: String,
|
||||
pub reason: Option<String>,
|
||||
/// 管理操作时填 user_id
|
||||
pub operator_id: Option<String>,
|
||||
/// 被操作资源类型:user | mailbox | email | attachment | blacklist | credit_rule
|
||||
pub target_type: Option<String>,
|
||||
/// 被操作资源 ID,统一使用字符串兼容不同主键类型
|
||||
pub target_id: Option<String>,
|
||||
/// 扩展审计上下文与批次 ID
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub metadata_json: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 来源 IP 黑名单(单 IP 或 CIDR,应用层匹配)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "blocked_ips")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub ip: String,
|
||||
pub reason: String,
|
||||
/// manual | auto
|
||||
pub source: String,
|
||||
pub expires_at: Option<DateTime>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 发件黑名单(domain | address | pattern)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "blocked_senders")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
/// domain | address | pattern
|
||||
pub kind: String,
|
||||
/// 如 flova.ai / spam@x.com / *@spam.com
|
||||
pub value: String,
|
||||
pub reason: String,
|
||||
/// manual | auto
|
||||
pub source: String,
|
||||
/// None=永久
|
||||
pub expires_at: Option<DateTime>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 用户积分账户(1:1 users)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "credit_accounts")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: String,
|
||||
pub balance: i64,
|
||||
/// 乐观锁版本号(并发扣减用)
|
||||
pub version: i64,
|
||||
pub total_granted: i64,
|
||||
pub total_consumed: i64,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, _insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
value.updated_at = Set(chrono::Utc::now().naive_utc());
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "credit_check_ins")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: String,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub date: chrono::NaiveDate,
|
||||
pub reward_granted: i64,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "credit_rules")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: i32,
|
||||
pub register_bonus: i64,
|
||||
pub daily_check_in_reward: i64,
|
||||
pub reward_balance_cap: i64,
|
||||
pub create_mailbox_cost: i64,
|
||||
pub receive_email_cost: i64,
|
||||
pub daily_mailboxes_limit: i32,
|
||||
pub daily_emails_limit: i32,
|
||||
pub version: i64,
|
||||
pub updated_by: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
if insert {
|
||||
value.created_at = Set(now);
|
||||
}
|
||||
value.updated_at = Set(now);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "credit_rule_changes")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub operator_id: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub previous_json: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub next_json: String,
|
||||
pub reason: String,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 积分流水(append-only,balance_after 保证可审计)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "credit_transactions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub user_id: String,
|
||||
/// 正=充值/赠送,负=消费
|
||||
pub delta: i64,
|
||||
/// 变更后余额(审计关键)
|
||||
pub balance_after: i64,
|
||||
/// register_bonus | create_mailbox | receive_email | admin_adjust | ...
|
||||
pub reason: String,
|
||||
pub related_mailbox_id: Option<i64>,
|
||||
pub related_email_id: Option<i64>,
|
||||
pub description: Option<String>,
|
||||
pub operator_id: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 每日额度计数(复合主键 user_id + date)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "daily_quotas")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: String,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub date: chrono::NaiveDate,
|
||||
pub mailboxes_created: i32,
|
||||
pub emails_received: i32,
|
||||
pub bytes_received: i64,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, _insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
value.updated_at = Set(chrono::Utc::now().naive_utc());
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 接收到的邮件(含安全与审计字段)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "emails")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
/// 所属邮箱 ID(FK mailboxes.id)
|
||||
pub mailbox_id: i64,
|
||||
/// 冗余:所属用户 ID,便于按用户聚合
|
||||
pub user_id: String,
|
||||
/// 完整收件地址
|
||||
pub recipient: String,
|
||||
/// envelope MAIL FROM
|
||||
pub mail_from: String,
|
||||
/// envelope RCPT TO
|
||||
pub rcpt_to: String,
|
||||
/// 发件人显示名
|
||||
pub sender_name: Option<String>,
|
||||
pub subject: Option<String>,
|
||||
pub text_body: Option<String>,
|
||||
pub html_body: Option<String>,
|
||||
/// 来源 IP
|
||||
pub source_ip: String,
|
||||
/// SMTP HELO/EHLO
|
||||
pub helo: Option<String>,
|
||||
/// PTR 反查结果
|
||||
pub ptr_result: Option<String>,
|
||||
/// SPF 结果
|
||||
pub spf_result: Option<String>,
|
||||
/// DKIM 结果
|
||||
pub dkim_result: Option<String>,
|
||||
/// DMARC 结果
|
||||
pub dmarc_result: Option<String>,
|
||||
/// 滥用评分 0-100
|
||||
pub abuse_score: i32,
|
||||
/// 状态:received | quarantined | rejected
|
||||
pub status: String,
|
||||
pub size_bytes: i64,
|
||||
/// Message-ID 头
|
||||
pub message_id: Option<String>,
|
||||
pub received_at: DateTime,
|
||||
/// 邮件保留期截止时间(默认 received + email_ttl_days)
|
||||
pub expires_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.received_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 邮件附件(小附件 inline 存 content;大附件后续可落盘存 storage_path)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "email_attachments")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
/// 所属邮件 ID(FK emails.id)
|
||||
pub email_id: i64,
|
||||
pub filename: Option<String>,
|
||||
pub content_type: String,
|
||||
pub size_bytes: i64,
|
||||
/// 二进制内容(小附件)
|
||||
pub content: Option<Vec<u8>>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "email_logs")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub user_id: Option<String>,
|
||||
pub recipient: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub error: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
if insert {
|
||||
value.created_at = Set(chrono::Utc::now().naive_utc());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 灰名单条目(triplet:sender_ip + mail_from + rcpt_to)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "greylist_entries")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub sender_ip: String,
|
||||
pub mail_from: String,
|
||||
pub rcpt_to: String,
|
||||
pub first_seen: DateTime,
|
||||
/// 期望重试时间(首次 + retry_delay)
|
||||
pub retry_after: DateTime,
|
||||
pub delivered: bool,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
if insert {
|
||||
value.created_at = Set(now);
|
||||
value.first_seen = Set(now);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 临时邮箱地址(用户创建,绑定 user,带过期与配额)
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "mailboxes")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
/// 所属用户 ID(FK users.id)
|
||||
pub user_id: String,
|
||||
/// 完整地址,如 abc123@mail.shenjianl.cn(唯一)
|
||||
#[sea_orm(unique)]
|
||||
pub address: String,
|
||||
/// 本地部分,如 abc123
|
||||
pub local_part: String,
|
||||
/// 域名,如 mail.shenjianl.cn
|
||||
pub domain: String,
|
||||
/// 邮箱级 access_token 的 SHA-256 哈希
|
||||
pub access_token_hash: String,
|
||||
/// 用途备注(可选)
|
||||
pub note: Option<String>,
|
||||
/// 状态:active | expired | revoked
|
||||
pub status: String,
|
||||
/// 单邮箱容量上限(字节)
|
||||
pub max_quota_bytes: i64,
|
||||
/// 已用容量(字节)
|
||||
pub used_bytes: i64,
|
||||
/// 邮箱过期时间
|
||||
pub expires_at: DateTime,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
if insert {
|
||||
value.created_at = Set(now);
|
||||
}
|
||||
value.updated_at = Set(now);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
pub mod abuse_event;
|
||||
pub mod abuse_rule;
|
||||
pub mod audit_log;
|
||||
pub mod blocked_ip;
|
||||
pub mod blocked_sender;
|
||||
pub mod credit_account;
|
||||
pub mod credit_check_in;
|
||||
pub mod credit_rule;
|
||||
pub mod credit_rule_change;
|
||||
pub mod credit_transaction;
|
||||
pub mod daily_quota;
|
||||
pub mod email;
|
||||
pub mod email_attachment;
|
||||
pub mod email_logs;
|
||||
pub mod greylist_entry;
|
||||
pub mod mailbox;
|
||||
pub mod user_profiles;
|
||||
pub mod users;
|
||||
@@ -0,0 +1,29 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user_profiles")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
async fn before_save<C: ConnectionTrait>(self, _: &C, insert: bool) -> Result<Self, DbErr> {
|
||||
let mut value = self;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
if insert {
|
||||
value.created_at = Set(now);
|
||||
}
|
||||
value.updated_at = Set(now);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "users")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
#[sea_orm(unique)]
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
/// 角色:user | admin
|
||||
pub role: String,
|
||||
/// 状态:active | suspended | banned
|
||||
pub status: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub deleted_at: Option<DateTime>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
/// 在保存前自动填充时间戳
|
||||
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
let mut this = self;
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
|
||||
if insert {
|
||||
// 插入时:设置创建时间和更新时间
|
||||
this.created_at = Set(now);
|
||||
this.updated_at = Set(now);
|
||||
} else {
|
||||
// 更新时:只更新更新时间
|
||||
this.updated_at = Set(now);
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod dto;
|
||||
pub mod entities;
|
||||
pub mod vo;
|
||||
@@ -0,0 +1,72 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// 注册结果
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RegisterResult {
|
||||
pub email: String,
|
||||
pub created_at: String, // ISO 8601 格式
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
impl From<(crate::domain::entities::users::Model, String, String)> for RegisterResult {
|
||||
fn from(
|
||||
(user_model, access_token, refresh_token): (
|
||||
crate::domain::entities::users::Model,
|
||||
String,
|
||||
String,
|
||||
),
|
||||
) -> Self {
|
||||
Self {
|
||||
email: user_model.email,
|
||||
created_at: user_model
|
||||
.created_at
|
||||
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
|
||||
.to_string(),
|
||||
access_token,
|
||||
refresh_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录结果
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoginResult {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub role: String,
|
||||
pub status: String,
|
||||
pub created_at: String, // ISO 8601 格式
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
impl From<(crate::domain::entities::users::Model, String, String)> for LoginResult {
|
||||
fn from(
|
||||
(user_model, access_token, refresh_token): (
|
||||
crate::domain::entities::users::Model,
|
||||
String,
|
||||
String,
|
||||
),
|
||||
) -> Self {
|
||||
Self {
|
||||
id: user_model.id,
|
||||
email: user_model.email,
|
||||
role: user_model.role,
|
||||
status: user_model.status,
|
||||
created_at: user_model
|
||||
.created_at
|
||||
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
|
||||
.to_string(),
|
||||
access_token,
|
||||
refresh_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新 Token 结果
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RefreshResult {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::domain::entities::{email, email_attachment};
|
||||
use serde::Serialize;
|
||||
|
||||
/// 邮件列表项(摘要)
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct EmailSummary {
|
||||
pub id: i64,
|
||||
pub mail_from: String,
|
||||
pub sender_name: Option<String>,
|
||||
pub subject: Option<String>,
|
||||
pub size_bytes: i64,
|
||||
pub status: String,
|
||||
pub received_at: String,
|
||||
}
|
||||
|
||||
impl From<email::Model> for EmailSummary {
|
||||
fn from(m: email::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
mail_from: m.mail_from,
|
||||
sender_name: m.sender_name,
|
||||
subject: m.subject,
|
||||
size_bytes: m.size_bytes,
|
||||
status: m.status,
|
||||
received_at: m.received_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 附件摘要
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AttachmentSummary {
|
||||
pub id: i64,
|
||||
pub filename: Option<String>,
|
||||
pub content_type: String,
|
||||
pub size_bytes: i64,
|
||||
}
|
||||
|
||||
impl From<email_attachment::Model> for AttachmentSummary {
|
||||
fn from(a: email_attachment::Model) -> Self {
|
||||
Self {
|
||||
id: a.id,
|
||||
filename: a.filename,
|
||||
content_type: a.content_type,
|
||||
size_bytes: a.size_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 邮件详情
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct EmailDetail {
|
||||
pub id: i64,
|
||||
pub mail_from: String,
|
||||
pub sender_name: Option<String>,
|
||||
pub subject: Option<String>,
|
||||
pub text_body: Option<String>,
|
||||
pub html_body: Option<String>,
|
||||
pub size_bytes: i64,
|
||||
pub status: String,
|
||||
pub received_at: String,
|
||||
pub attachments: Vec<AttachmentSummary>,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::domain::entities::mailbox;
|
||||
use serde::Serialize;
|
||||
|
||||
/// 邮箱视图
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MailboxVO {
|
||||
pub id: i64,
|
||||
pub address: String,
|
||||
pub local_part: String,
|
||||
pub domain: String,
|
||||
pub note: Option<String>,
|
||||
pub status: String,
|
||||
pub max_quota_bytes: i64,
|
||||
pub used_bytes: i64,
|
||||
pub expires_at: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl From<mailbox::Model> for MailboxVO {
|
||||
fn from(m: mailbox::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
address: m.address,
|
||||
local_part: m.local_part,
|
||||
domain: m.domain,
|
||||
note: m.note,
|
||||
status: m.status,
|
||||
max_quota_bytes: m.max_quota_bytes,
|
||||
used_bytes: m.used_bytes,
|
||||
expires_at: m.expires_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
|
||||
created_at: m.created_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建邮箱结果(含一次性返回的明文 access_token)
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateMailboxResult {
|
||||
#[serde(flatten)]
|
||||
pub mailbox: MailboxVO,
|
||||
pub access_token: String,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
pub mod auth;
|
||||
pub mod email;
|
||||
pub mod mailbox;
|
||||
pub mod user;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
/// 统一的 API 响应结构
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
/// HTTP 状态码
|
||||
pub code: u16,
|
||||
/// 响应消息
|
||||
pub message: String,
|
||||
/// 响应数据
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: Serialize> ApiResponse<T> {
|
||||
/// 成功响应(200)
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
code: 200,
|
||||
message: "Success".to_string(),
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// 成功响应(自定义消息)
|
||||
pub fn success_with_message(data: T, message: &str) -> Self {
|
||||
Self {
|
||||
code: 200,
|
||||
message: message.to_string(),
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// 错误响应
|
||||
#[allow(dead_code)]
|
||||
pub fn error(status_code: StatusCode, message: &str) -> ApiResponse<()> {
|
||||
ApiResponse {
|
||||
code: status_code.as_u16(),
|
||||
message: message.to_string(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 错误响应(带数据)
|
||||
#[allow(dead_code)]
|
||||
pub fn error_with_data(status_code: StatusCode, message: &str, data: T) -> ApiResponse<T> {
|
||||
ApiResponse {
|
||||
code: status_code.as_u16(),
|
||||
message: message.to_string(),
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// 用户相关 VO(预留)
|
||||
Reference in New Issue
Block a user