2025 终极指南:axum 邮件发送全攻略——从 SMTP 配置到动态模板引擎集成
引言:你还在为 Rust Web 应用的邮件功能头疼吗?
在现代 Web 开发中,邮件通知系统是用户认证、交易确认、活动提醒等核心功能的基础组件。作为基于 Tokio、Tower 和 Hyper 构建的现代化 Rust Web 框架,axum 以其出色的异步性能和模块化设计赢得了开发者的青睐。然而,许多开发者在实现邮件功能时仍面临三大痛点:
- 配置复杂:SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)参数繁多,SSL/TLS 配置容易出错
- 模板混乱:HTML 邮件拼接导致代码可读性差,维护成本高
- 错误处理:异步邮件发送与 axum 响应周期的协同难题
本文将通过 5 个实战模块,带你从零构建企业级邮件系统,涵盖从基础配置到高级模板引擎集成的完整流程。读完本文,你将掌握:
✅ 基于 lettre 库的 SMTP 客户端实现
✅ Tera 模板引擎与 axum 的无缝集成
✅ 异步邮件发送的错误处理最佳实践
✅ 生产环境的性能优化与监控方案
✅ 完整代码示例与可复用组件
模块一:核心依赖与项目初始化
1.1 必备依赖库解析
实现企业级邮件功能需要以下关键依赖:
| 依赖名称 | 功能描述 | 版本要求 | 关键特性 |
|---|---|---|---|
lettre | Rust 生态最成熟的邮件发送库 | ≥0.10.0 | 支持 SMTP/ESMTP、HTML 邮件、附件 |
lettre_stub_transport | 测试环境的邮件拦截器 | ≥0.10.0 | 捕获发送的邮件用于测试验证 |
lettre_smtp_transport | SMTP 传输协议实现 | ≥0.10.0 | 支持 STARTTLS、身份验证、连接池 |
lettre_email | 邮件构建器 | ≥0.10.0 | 类型安全的邮件内容构造 API |
tera | 高性能模板引擎 | ≥1.0.0 | Jinja2 语法兼容,支持模板继承 |
serde | 数据序列化框架 | ≥1.0 | 模板数据模型的序列化支持 |
async-std | 异步运行时 | ≥1.0 | 部分依赖需要的异步支持 |
1.2 Cargo.toml 配置示例
[package]
name = "axum-email-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7", features = ["headers", "json"] }
tokio = { version = "1.0", features = ["full"] }
lettre = { version = "0.10", features = ["smtp-transport", "builder", "native-tls", "json-parser"] }
tera = { version = "1.19", features = ["chrono"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
chrono = { version = "0.4", features = ["serde"] }
1.3 项目结构设计
采用领域驱动的模块化结构,将邮件功能封装为独立组件:
src/
├── main.rs # 应用入口,路由定义
├── email/ # 邮件功能模块
│ ├── mod.rs # 公共接口
│ ├── client.rs # SMTP 客户端配置
│ ├── template.rs # 模板引擎集成
│ └── error.rs # 错误类型定义
├── handlers/ # 请求处理器
│ ├── mod.rs
│ └── email_handler.rs # 邮件发送相关路由处理
└── templates/ # 邮件模板文件
├── base.html # 基础模板
├── verification.html # 邮箱验证模板
└── notification.html # 通用通知模板
模块二:SMTP 客户端实现与配置
2.1 邮件客户端核心组件
邮件客户端需要处理 SMTP 连接管理、认证和邮件发送。以下是 email/client.rs 的实现:
use std::time::Duration;
use lettre::transport::smtp::{
authentication::Credentials,
client::{Tls, TlsParameters}
};
use lettre::{Message, Address, Transport, SmtpTransport};
use crate::email::error::EmailError;
/// SMTP 客户端配置
#[derive(Debug, Clone)]
pub struct SmtpConfig {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub from_address: String,
pub tls_enabled: bool,
}
/// 邮件客户端
#[derive(Clone)]
pub struct EmailClient {
sender: SmtpTransport,
from_address: Address,
}
impl EmailClient {
/// 创建新的邮件客户端
pub fn new(config: SmtpConfig) -> Result<Self, EmailError> {
// 解析发件人地址
let from_address = config.from_address.parse()
.map_err(|_| EmailError::InvalidAddress(config.from_address))?;
// 构建 SMTP 传输器
let sender = if config.tls_enabled {
// TLS 加密连接
SmtpTransport::relay(&config.host)?
.port(config.port)
.credentials(Credentials::new(
config.username.clone(),
config.password.clone()
))
.build()
} else {
// STARTTLS 连接
SmtpTransport::relay(&config.host)?
.port(config.port)
.credentials(Credentials::new(
config.username.clone(),
config.password.clone()
))
.tls().starttls()
.build()
};
Ok(Self { sender, from_address })
}
/// 发送纯文本邮件
pub fn send_text(
&self,
to: &str,
subject: &str,
body: &str
) -> Result<(), EmailError> {
let to_address: Address = to.parse()?;
let email = Message::builder()
.from(self.from_address.clone())
.to(to_address)
.subject(subject)
.body(body.to_string())?;
self.sender.send(&email)?;
Ok(())
}
/// 发送 HTML 邮件
pub fn send_html(
&self,
to: &str,
subject: &str,
html_body: &str,
text_body: &str
) -> Result<(), EmailError> {
let to_address: Address = to.parse()?;
let email = Message::builder()
.from(self.from_address.clone())
.to(to_address)
.subject(subject)
.header(lettre::message::header::ContentType::TEXT_HTML)
.body(html_body.to_string())?;
self.sender.send(&email)?;
Ok(())
}
}
2.2 环境配置与多环境支持
为支持开发、测试和生产环境的不同配置,我们使用环境变量管理 SMTP 参数。创建 email/config.rs:
use std::env;
use crate::email::client::SmtpConfig;
use crate::email::error::EmailError;
impl SmtpConfig {
/// 从环境变量加载配置
pub fn from_env() -> Result<Self, EmailError> {
Ok(Self {
host: env::var("SMTP_HOST")
.unwrap_or_else(|_| "smtp.example.com".to_string()),
port: env::var("SMTP_PORT")
.unwrap_or_else(|_| "587".to_string())
.parse()?,
username: env::var("SMTP_USERNAME")
.ok_or(EmailError::MissingConfig("SMTP_USERNAME".to_string()))?,
password: env::var("SMTP_PASSWORD")
.ok_or(EmailError::MissingConfig("SMTP_PASSWORD".to_string()))?,
from_address: env::var("SMTP_FROM_ADDRESS")
.ok_or(EmailError::MissingConfig("SMTP_FROM_ADDRESS".to_string()))?,
tls_enabled: env::var("SMTP_TLS_ENABLED")
.unwrap_or_else(|_| "true".to_string())
.parse()?,
})
}
}
2.3 错误处理策略
定义专用的邮件错误类型 email/error.rs:
use std::fmt;
use lettre::address::AddressError;
use lettre::transport::smtp::Error as SmtpError;
use lettre::message::error::MessageError;
#[derive(Debug)]
pub enum EmailError {
InvalidAddress(String),
SmtpError(SmtpError),
MessageError(MessageError),
MissingConfig(String),
TemplateError(tera::Error),
IoError(std::io::Error),
ParseError(String),
}
impl fmt::Display for EmailError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
EmailError::InvalidAddress(addr) => write!(f, "Invalid email address: {}", addr),
EmailError::SmtpError(e) => write!(f, "SMTP error: {}", e),
EmailError::MessageError(e) => write!(f, "Email message error: {}", e),
EmailError::MissingConfig(key) => write!(f, "Missing configuration: {}", key),
EmailError::TemplateError(e) => write!(f, "Template error: {}", e),
EmailError::IoError(e) => write!(f, "I/O error: {}", e),
EmailError::ParseError(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for EmailError {}
// 错误转换实现
impl From<AddressError> for EmailError {
fn from(err: AddressError) -> Self {
EmailError::InvalidAddress(err.to_string())
}
}
impl From<SmtpError> for EmailError {
fn from(err: SmtpError) -> Self {
EmailError::SmtpError(err)
}
}
impl From<MessageError> for EmailError {
fn from(err: MessageError) -> Self {
EmailError::MessageError(err)
}
}
impl From<tera::Error> for EmailError {
fn from(err: tera::Error) -> Self {
EmailError::TemplateError(err)
}
}
impl From<std::io::Error> for EmailError {
fn from(err: std::io::Error) -> Self {
EmailError::IoError(err)
}
}
impl From<std::num::ParseIntError> for EmailError {
fn from(err: std::num::ParseIntError) -> Self {
EmailError::ParseError(err.to_string())
}
}
impl From<std::str::ParseBoolError> for EmailError {
fn from(err: std::str::ParseBoolError) -> Self {
EmailError::ParseError(err.to_string())
}
}
模块三:模板引擎集成与动态内容生成
3.1 Tera 模板引擎配置
在 email/template.rs 中实现模板引擎初始化:
use std::path::PathBuf;
use tera::{Tera, Context};
use crate::email::error::EmailError;
/// 邮件模板引擎
#[derive(Debug, Clone)]
pub struct EmailTemplateEngine {
tera: Tera,
}
impl EmailTemplateEngine {
/// 创建新的模板引擎实例
pub fn new(template_dir: &str) -> Result<Self, EmailError> {
let mut tera = Tera::new(&format!("{}/**/*.html", template_dir))?;
// 启用自动转义以防止 XSS
tera.autoescape_on(vec![".html"]);
Ok(Self { tera })
}
/// 从环境变量加载模板目录
pub fn from_env() -> Result<Self, EmailError> {
let template_dir = std::env::var("EMAIL_TEMPLATE_DIR")
.unwrap_or_else(|_| "templates".to_string());
Self::new(&template_dir)
}
/// 渲染模板
pub fn render(
&self,
template_name: &str,
context: &Context
) -> Result<String, EmailError> {
let rendered = self.tera.render(template_name, context)?;
Ok(rendered)
}
/// 渲染邮件模板(同时生成 HTML 和文本版本)
pub fn render_email(
&self,
template_name: &str,
context: &Context
) -> Result<(String, String), EmailError> {
let html_body = self.render(&format!("{}.html", template_name), context)?;
let text_body = self.render(&format!("{}.txt", template_name), context)?;
Ok((html_body, text_body))
}
}
3.2 模板文件结构与继承
创建基础模板 templates/base.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}通知邮件{% endblock %}</title>
<style>
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #f5f5f5; padding: 10px 20px; border-radius: 5px 5px 0 0; }
.content { padding: 20px; border: 1px solid #e0e0e0; border-top: none; }
.footer { margin-top: 20px; font-size: 12px; color: #666; text-align: center; }
.button {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>{% block header %}通知{% endblock %}</h2>
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
<div class="footer">
<p>这是一封自动发送的邮件,请勿直接回复。</p>
{% block footer %}{% endblock %}
</div>
</div>
</body>
</html>
邮箱验证模板 templates/verification.html:
{% extends "base.html" %}
{% block title %}邮箱验证 - {{ app_name }}{% endblock %}
{% block header %}邮箱验证{% endblock %}
{% block content %}
<p>您好,{{ username }}!</p>
<p>感谢您注册 {{ app_name }}。请点击下方按钮完成邮箱验证:</p>
<a href="{{ verification_url }}" class="button">验证邮箱</a>
<p>如果您没有注册过 {{ app_name }},请忽略此邮件。</p>
<p>此验证链接有效期为 {{ expires_in }} 小时。</p>
{% endblock %}
{% block footer %}
<p>{{ app_name }} 团队</p>
<p>{{ current_year }} 版权所有</p>
{% endblock %}
对应的文本模板 templates/verification.txt:
您好,{{ username }}!
感谢您注册 {{ app_name }}。请访问以下链接完成邮箱验证:
{{ verification_url }}
如果您没有注册过 {{ app_name }},请忽略此邮件。
此验证链接有效期为 {{ expires_in }} 小时。
--
{{ app_name }} 团队
{{ current_year }} 版权所有
3.3 动态数据与上下文管理
创建 email/data.rs 定义常用邮件模板数据结构:
use chrono::{Local, DateTime};
use serde::Serialize;
/// 邮箱验证邮件数据
#[derive(Debug, Serialize)]
pub struct VerificationEmailData {
pub username: String,
pub app_name: String,
pub verification_url: String,
pub expires_in: u8,
pub current_year: i32,
}
impl VerificationEmailData {
/// 创建新的验证邮件数据
pub fn new(
username: String,
app_name: String,
verification_url: String,
expires_in: u8
) -> Self {
Self {
username,
app_name,
verification_url,
expires_in,
current_year: Local::now().year(),
}
}
}
/// 通用通知邮件数据
#[derive(Debug, Serialize)]
pub struct NotificationEmailData {
pub username: String,
pub title: String,
pub message: String,
pub action_url: Option<String>,
pub action_text: Option<String>,
pub app_name: String,
pub current_year: i32,
}
impl NotificationEmailData {
/// 创建新的通知邮件数据
pub fn new(
username: String,
title: String,
message: String,
app_name: String
) -> Self {
Self {
username,
title,
message,
action_url: None,
action_text: None,
app_name,
current_year: Local::now().year(),
}
}
/// 添加行动按钮
pub fn with_action(mut self, url: String, text: String) -> Self {
self.action_url = Some(url);
self.action_text = Some(text);
self
}
}
模块四:axum 路由集成与异步处理
4.1 邮件发送中间件与状态管理
在 main.rs 中初始化邮件客户端和模板引擎,并将其作为状态注入 axum 应用:
use axum::{Router, Server, Extension};
use email::{EmailClient, EmailTemplateEngine};
use handlers::email_handler;
mod email;
mod handlers;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 初始化日志
tracing_subscriber::fmt::init();
// 初始化邮件客户端
let smtp_config = email::client::SmtpConfig::from_env()?;
let email_client = EmailClient::new(smtp_config)?;
// 初始化模板引擎
let template_engine = EmailTemplateEngine::from_env()?;
// 构建路由
let app = Router::new()
.route("/send-verification", axum::routing::post(email_handler::send_verification))
.route("/send-notification", axum::routing::post(email_handler::send_notification))
.layer(Extension(email_client))
.layer(Extension(template_engine));
// 启动服务器
let addr = "0.0.0.0:3000".parse()?;
tracing::info!("服务器运行在 http://{}", addr);
Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
4.2 邮件发送处理器实现
创建 handlers/email_handler.rs:
use axum::{
extract::{Extension, Json, Form},
response::{Html, IntoResponse, Json as AxumJson},
http::StatusCode,
};
use tera::Context;
use crate::email::{
EmailClient, EmailTemplateEngine,
data::{VerificationEmailData, NotificationEmailData}
};
use crate::email::error::EmailError;
/// 验证邮件请求数据
#[derive(Debug, serde::Deserialize)]
pub struct SendVerificationRequest {
pub email: String,
pub username: String,
pub verification_url: String,
}
/// 发送验证邮件
pub async fn send_verification(
Extension(email_client): Extension<EmailClient>,
Extension(template_engine): Extension<EmailTemplateEngine>,
Json(request): Json<SendVerificationRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
// 准备模板数据
let data = VerificationEmailData::new(
request.username.clone(),
"axum 邮件示例应用".to_string(),
request.verification_url.clone(),
24, // 有效期 24 小时
);
// 创建模板上下文
let mut context = Context::new();
context.insert("data", &data);
// 渲染模板
let (html_body, text_body) = template_engine
.render_email("verification", &context)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("模板渲染失败: {}", e)))?;
// 发送邮件
email_client.send_html(
&request.email,
&format!("[{}] 请验证您的邮箱", data.app_name),
&html_body,
&text_body
).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("邮件发送失败: {}", e)))?;
Ok(AxumJson(serde_json::json!({
"status": "success",
"message": "验证邮件已发送",
"email": request.email
})))
}
/// 通知邮件请求数据
#[derive(Debug, serde::Deserialize)]
pub struct SendNotificationRequest {
pub email: String,
pub username: String,
pub title: String,
pub message: String,
pub action_url: Option<String>,
pub action_text: Option<String>,
}
/// 发送通知邮件
pub async fn send_notification(
Extension(email_client): Extension<EmailClient>,
Extension(template_engine): Extension<EmailTemplateEngine>,
Json(request): Json<SendNotificationRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
// 准备模板数据
let mut data = NotificationEmailData::new(
request.username.clone(),
request.title.clone(),
request.message.clone(),
"axum 邮件示例应用".to_string(),
);
// 添加行动按钮(如果提供)
if let (Some(url), Some(text)) = (request.action_url.clone(), request.action_text.clone()) {
data = data.with_action(url, text);
}
// 创建模板上下文
let mut context = Context::new();
context.insert("data", &data);
// 渲染模板
let (html_body, text_body) = template_engine
.render_email("notification", &context)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("模板渲染失败: {}", e)))?;
// 发送邮件
email_client.send_html(
&request.email,
&request.title,
&html_body,
&text_body
).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("邮件发送失败: {}", e)))?;
Ok(AxumJson(serde_json::json!({
"status": "success",
"message": "通知邮件已发送",
"email": request.email
})))
}
4.3 表单提交与前端集成
为支持表单提交,添加表单处理路由:
/// 表单提交发送邮件
pub async fn send_verification_form(
Extension(email_client): Extension<EmailClient>,
Extension(template_engine): Extension<EmailTemplateEngine>,
Form(request): Form<SendVerificationRequest>,
) -> Result<Html<String>, (StatusCode, String)> {
// 复用前面的逻辑发送邮件...
Ok(Html(format!(
r#"
<!DOCTYPE html>
<html>
<head><title>邮件发送成功</title></head>
<body>
<h1>邮件发送成功</h1>
<p>验证邮件已发送至: {}</p>
<p><a href="/">返回首页</a></p>
</body>
</html>
"#,
request.email
)))
}
模块五:测试、监控与生产环境优化
5.1 单元测试与集成测试
创建 tests/email_test.rs:
use axum::Extension;
use lettre::transport::stub::StubTransport;
use crate::email::{EmailClient, EmailTemplateEngine};
#[tokio::test]
async fn test_send_verification_email() {
// 使用 StubTransport 捕获发送的邮件
let transport = StubTransport::new_ok();
let email_client = EmailClient {
sender: transport.clone(),
from_address: "test@example.com".parse().unwrap(),
};
// 创建模板引擎(使用测试模板目录)
let template_engine = EmailTemplateEngine::new("tests/templates")
.expect("模板引擎初始化失败");
// 准备测试数据
let data = crate::email::data::VerificationEmailData::new(
"test_user".to_string(),
"测试应用".to_string(),
"http://example.com/verify?token=test".to_string(),
24,
);
// 渲染模板
let mut context = tera::Context::new();
context.insert("data", &data);
let (html_body, text_body) = template_engine.render_email("verification", &context)
.expect("模板渲染失败");
// 发送测试邮件
email_client.send_html(
"recipient@example.com",
"测试邮件主题",
&html_body,
&text_body
).expect("邮件发送失败");
// 验证邮件是否被正确发送
let sent_emails = transport.messages();
assert_eq!(sent_emails.len(), 1);
let sent_email = &sent_emails[0];
assert_eq!(sent_email.to().to_string(), "recipient@example.com");
assert!(sent_email.body().contains("测试应用"));
assert!(sent_email.body().contains("http://example.com/verify?token=test"));
}
5.2 性能优化策略
- 连接池与连接复用
// 在 email/client.rs 中添加连接池配置
use lettre::transport::smtp::pool::{Pool, PoolConfig};
impl EmailClient {
/// 创建带连接池的 SMTP 客户端
pub fn with_pool(config: SmtpConfig, pool_size: usize) -> Result<Self, EmailError> {
let transport = SmtpTransport::relay(&config.host)?
.port(config.port)
.credentials(Credentials::new(
config.username.clone(),
config.password.clone()
))
.build();
// 配置连接池
let pool_config = PoolConfig::new()
.max_size(pool_size)
.idle_timeout(Some(std::time::Duration::from_secs(60)));
let sender = Pool::new(transport, pool_config);
Ok(Self {
sender: sender.into(),
from_address: config.from_address.parse()?,
})
}
}
- 异步发送与后台任务
使用 axum 的 spawn_blocking 或专门的任务队列处理邮件发送,避免阻塞请求处理:
use axum::extract::State;
use tokio::task;
/// 异步发送邮件(不阻塞响应)
pub async fn send_email_async<F>(f: F)
where
F: FnOnce() -> Result<(), EmailError> + Send + 'static,
{
task::spawn_blocking(move || {
if let Err(e) = f() {
tracing::error!("异步邮件发送失败: {}", e);
}
});
}
// 在处理器中使用
pub async fn send_verification_async(
// ... 其他参数
) -> impl IntoResponse {
// 准备数据和渲染模板...
// 异步发送邮件,不阻塞响应
send_email_async(move || {
email_client.send_html(
&request.email,
&subject,
&html_body,
&text_body
)
}).await;
// 立即返回响应,无需等待邮件发送完成
Ok(AxumJson(serde_json::json!({
"status": "success",
"message": "邮件正在后台发送"
})))
}
5.3 监控与日志
为邮件发送添加详细日志:
// 在 email/client.rs 的 send_html 方法中添加日志
use tracing::{info, warn, error};
pub fn send_html(
&self,
to: &str,
subject: &str,
html_body: &str,
text_body: &str
) -> Result<(), EmailError> {
let to_address: Address = to.parse()?;
let email = Message::builder()
.from(self.from_address.clone())
.to(to_address.clone())
.subject(subject)
.header(lettre::message::header::ContentType::TEXT_HTML)
.body(html_body.to_string())?;
info!(
"准备发送邮件: 发件人={}, 收件人={}, 主题={}",
self.from_address, to_address, subject
);
match self.sender.send(&email) {
Ok(_) => {
info!(
"邮件发送成功: 发件人={}, 收件人={}, 主题={}",
self.from_address, to_address, subject
);
Ok(())
}
Err(e) => {
error!(
"邮件发送失败: 发件人={}, 收件人={}, 主题={}, 错误={}",
self.from_address, to_address, subject, e
);
Err(EmailError::SmtpError(e))
}
}
}
结论:构建企业级邮件系统的最佳实践
通过本文的实战指南,我们构建了一个功能完善的 axum 邮件系统,涵盖:
- 模块化架构:将邮件功能封装为独立模块,提高代码复用性和可维护性
- 环境适配:通过环境变量配置支持多环境部署,简化 CI/CD 流程
- 安全最佳实践:模板自动转义防止 XSS,敏感配置通过环境变量注入
- 性能优化:连接池复用 SMTP 连接,异步发送避免阻塞请求处理
- 错误处理:自定义错误类型和详细日志,便于问题诊断和排查
进阶学习路线
- 邮件队列:集成 Redis 或 RabbitMQ 实现可靠的邮件队列,处理高峰期负载
- 批量发送:实现邮件列表管理和批量发送功能,支持退订机制
- 邮件跟踪:添加打开率和点击率跟踪
- 国际化:多语言邮件模板支持
- DKIM/SPF 配置:提高邮件送达率,避免被标记为垃圾邮件
项目资源
- 完整代码示例:axum-email-demo
- 依赖文档:
- lettre: https://docs.rs/lettre
- tera: https://docs.rs/tera
- axum: https://docs.rs/axum
希望本文能帮助你在 axum 应用中构建可靠、高效的邮件系统。如有任何问题或建议,欢迎在评论区留言讨论!
点赞 + 收藏 + 关注,获取更多 Rust Web 开发实战教程!下期预告:axum 与 gRPC 集成实战。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



