如何在Rust中开发一个电子邮件服务器软件?

22-09-23 banq

1、设置新项目:

# create a new Rust binary application
cargo new rust-axum-email-server

navigate to the project directory 
cd rust-axum-email-server

fire up the server 
cargo watch -x run



2、准备依赖包:
在项目的根目录下打开一个新的命令行解释器,然后执行以下命令:

install dependencies
cargo add axum tokio -F tokio/full lettre serde -F serde/derive dotenv


通过参数-F <crate>/<feature>. 下载拉入以下依赖:
  • axum,我们的 Web 框架
  • tokio的全部功能, Rust 的异步运行时
  • lettre crate,Rust 的邮件程序库
  • serde的派生特性 ,一个用于解析 JSON 的 crate,
  • dotenv用于在开发中解析环境变量

此时,我们的应用程序清单 ( Cargo.toml) 将如下所示:

[package]
name = "rust-axum-email-server"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

<p class="indent">[dependencies]
axum = "0.5.16"
dotenv = "0.15.0"
lettre = "0.10.1"
serde = { version = "1.0.144", features = ["derive"] }
tokio = { version = "1.21.1", features = ["full"] }



3、编码
切换导航到src/main.rs内容并将其替换为以下列表:

use axum::{response::Html, routing::get, Router};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // build our application with a route
    let app = Router::new().route("/", get(handler));

    // run it
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on http://{}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}


这段是axum hello-word example的摘录。

我们首先从 axum 导入所需的模块以及SocketAddr从 Rust 标准库导入的模块,这SocketAddr是一个 IP 地址构建器。我们正在使用它来构造 localhost IP 地址,如下所示:

  let addr = SocketAddr::from(([127, 0, 0, 1], 3000));


最后,我们将地址传递给我们的 Axum 服务器实例:

...
  axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();



Axum 使用“handler”来表示我们通常在Node 的 Expressjs的controller等框架中类似功能。
控制器(或handler)本质上是接受和解析我们的 HTTP 请求并处理它们以返回 HTTP 响应的函数或方法。

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}


我们的服务器已将一个处理程序安装到基本路由,如下所示:

...
   // build our application with a route
    let app = Router::new().route("/", get(handler));

...


一旦您在浏览器中访问https://127.0.0.1:3000,这个路由就会打印出“Hello World”。

让我们继续创建一个处理程序来发送我们的电子邮件。就在我们这样做之前,我们需要创建一个.env文件来保存我们的环境变量。

create a .env file
touch .env


填充 . env包含以下字段及其对应值的文件:

the SMTP username, typically the full email address
SMTP_USER=

the SMTP password
SMTP_PASSWORD=

SMTP host
SMTP_HOST=


现在,让我们到 src/main.rs 创建我们的处理程序,一旦完成,我们将把处理程序挂载到路由中。
要做到这一点,请将 src/main.rs 的内容替换为下面的列表,注意注释和添加的片段:

use axum::{
    response::{Html, IntoResponse},
    routing::{get, post},
    Json, Router,
};
use dotenv::dotenv; // import the dotenv crate for parsing the `.env file`
use serde::{Deserialize, Serialize};
use std::env; //for getting fields from the environment
use std::net::SocketAddr; // import serde for parsing our struct to and from Json
                          //import the email library needed modules
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};

/// define a structure that maps to the format of our HTTP request body
/// derive the Debug trait, this will allow, printing the struct in stdout
/// derive the Serializing trait, this will allow building up JSON
/// derive the Deserializing trait
#[derive(Debug, Serialize, Deserialize)]
struct EmailPayload {
    fullname: String,
    email: String,
    message: String,
}

//mount the tokio runtime to allow our main function to support asynchronous execution
#[tokio::main]
async fn main() {
    dotenv().ok();
    // build our application with a route
    let app = Router::new()
        .route("/", get(handler))
        //mount the handle to a path, using the HTTP POST verb
        .route("/send-email", post(dispatch_email));

    // run it
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on http://{}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

/// define an email handler, the handler will take the user's email address and the message body
/// the parsed payload will be fed into the `lettre` library and finally, a response will be returned
async fn dispatch_email(Json(payload): Json<EmailPayload>) -> impl IntoResponse {
    // println!("{:#?}", payload);
    //destructure the HTTP request body
    let EmailPayload {
        email,
        message,
        fullname,
    } = &payload;

    //contruct emil config
    let from_address = String::from("You <you@yordomain.com>");
    let to_address = format!("{fullname} <{email}>");
    let reply_to = String::from("You <you@yordomain.com>");
    let email_subject = "Axum Rust tutorial";

    let email = Message::builder()
        .from(from_address.parse().unwrap())
        .reply_to(reply_to.parse().unwrap())
        .to(to_address.parse().unwrap())
        .subject(email_subject)
        .body(String::from(message))
        .unwrap();

    let creds = Credentials::new(
        env::var("SMTP_USERNAME").expect("SMTP Username not specified "),
        env::var("SMTP_PASSWORD").expect("SMTP Password not specified"),
    );

    // Open a remote connection to SMTP server
    let mailer = SmtpTransport::relay(&env::var("SMTP_HOST").expect("SMTP Host not specified"))
        .unwrap()
        .credentials(creds)
        .build();

    // Send the email
    match mailer.send(&email) {
        Ok(_) => println!("Email sent successfully!"),
        Err(e) => panic!("Could not send email: {:?}", e),
    }
}


解释
  • 我首先从每个 crate 导入所需的模块

...
use dotenv::dotenv; // import the dotenv crate for parsing the `.env file`
use serde::{Deserialize, Serialize};
use std::env; //for getting fields from the environment
use std::net::SocketAddr; // import serde for parsing our struct to and from Json
                          //import the email library needed modules
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
...

  • 我定义了一个包含 HTTP 有效负载的数据结构,这是Axum Extractor所要求的,我将在以下部分中讨论它。

...
#[derive(Debug, Serialize, Deserialize)]
struct EmailPayload {
    fullname: String,
    email: String,
    message: String,
}

...

  • 我初始化了dotenv crate以允许在开发中解析环境变量,然后我使用Axum的post方法将路由处理程序安装到/send-email路由上,处理服务器接受的请求。

...
    dotenv().ok();
    // build our application with a route
    let app = Router::new()
        .route("/", get(handler))
        //mount the handle to a path, using the HTTP POST verb
        .route("/send-email", post(dispatch_email));
...



总结部分基本上是,插入我们的环境变量,解析我们的 HTTP 请求负载并发送电子邮件,并在控制台中打印响应。

测试
对于测试,您可以使用任何您熟悉的 HTTP 客户端,最常见的是Curl和 Postman
但是,我发现Thunder Client使用起来更方便,因为它是一个 VS Code 扩展。这意味着我可以在舒适的情况下做任何事情。