用Rust发一封图文并茂的邮件

文章目录

这篇文章的内容其实和之前的文章《用Python发一封图文并茂的邮件》差不多,跟之前不同之处在于之前是使用Python来完成邮件的发送任务,而本文使用Rust发送邮件而已。

虽然我们已经有了各种各样的通讯软件和APP但是邮件还是一直占据一席之地的,这可能主要在于邮件有比较强的表达能力(即可以使用HTML语法), 再者就是它比较易用和通用,而其他通讯软件有各种各样的限制。

Cargo.toml依赖如下

[dependencies]
lettre = "0.11.4"

值得注意的是: 对于非1.0的包,如果打算使用最新的包一定要参考最新的官方示例代码, 因为作者可能会改接口。

快速入门

以下代码直接改自https://github.com/lettre/lettre.

use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};

fn main() {
    let from_address = "你的邮箱";
    // 大家可以测试的时候可以随便往这个邮箱发....
    let to_address = "[email protected]";
    let subject = "用Rust发一封简单的邮件";
    let content = String::from("做人嘛, 最重要是开心啦...");

    // 认证相关
    let username = String::from("你的邮箱");
    let password = String::from("你的密码");
    let smtp_server = "smtp.126.com";

    let email = Message::builder()
        .from(from_address.parse().expect("解析发送邮箱地址失败:"))
        // 这个reply_to我没用过就去掉了.
        .to(to_address.parse().expect("解析收件邮箱地址失败: "))
        .subject(subject)
        .header(ContentType::TEXT_PLAIN)
        .body(content)
        .expect("构造邮件消息体失败:");

    let creds = Credentials::new(username, password);

    // 默认使用TLS连接
    let mailer = SmtpTransport::relay(smtp_server)
        .expect("解析SMTP服务器失败:")
        .credentials(creds)
        .build();

    match mailer.send(&email) {
        Ok(_) => println!("邮件发送成功!"),
        Err(e) => panic!("发送邮件失败: {e:?}"),
    }
}

上面的代码使用126邮箱测试是没有问题的,QQ邮箱没有测试过。

值得注意的是,国内的知名邮箱服务如126, 163, qq等,都会提供一个专门用于SMTP接口发送邮件的密码,如果你直接使用126邮箱的密码,或者qq密码来发送邮件那是不会成功的。

这里提供一个126邮箱获取授权码(SMTP接口登录的密码)的链接: https://help.lunkr.cn/126-IMAP-Login-Tutorial/IMAP-Login-Tutorial-126.html, 如果不行的话可以自行再搜索一下。

邮件的格式

邮件的格式主要就两种: plainhtml

plain就像一个普通的文本, 没有格式,也就是上面代码的例子,这个并没有太多可以说的。 html正如其名, 是html的格式,相当于一个邮件就是一个静态的网页,这样的话可玩性就很高了,你可以通过css控制表现形式.

注意: 这里的css虽然语法一样,但,是否与浏览器渲染结果完全一致, 是不一定的。

那么可能有人要问了,我要发一个动态的网页怎么办? 发个链接呀

发送HTML格式的邮件

与纯文本的主要不同在于设置的ContentType不同, 以及请求头body的内容不同(这是当然的啦, HTML肯定得是一个合法的HTML格式内容。)

use lettre::{
    message::{header, MultiPart, SinglePart},
    SmtpTransport, Message, Transport,
};
use lettre::transport::smtp::authentication::Credentials;

fn main() {
    // 邮件内容
    let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>来自Rust代码发送的邮件</title>
</head>
<body>
    <div style="display: flex; flex-direction: column; align-items: center;">
        <h2 style="font-family: Arial, Helvetica, sans-serif;">做人嘛, 最重要是开心啦...</h2>
    </div>
</body>
</html>"#;

    // 邮件的参数
    let from_address = "你的邮箱";
    // 大家可以测试的时候可以随便往这个邮箱发....
    let to_address = "[email protected]";
    let subject = "用Rust发一封HTML格式的邮件";

    // 认证相关
    let username = String::from("你的邮箱");
    let password = String::from("你的密码");
    let smtp_server = "smtp.126.com";

    // 构造邮件体内容
    let email = Message::builder()
        .from(from_address.parse().expect("解析发送邮箱地址失败:"))
        .to(to_address.parse().expect("解析收件邮箱地址失败: "))
        .subject(subject)
        .multipart(
            MultiPart::alternative() //  alternative构造一个可回退的邮件体
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentType::TEXT_PLAIN)
                        // 应该是为了防止邮件服务器不支持HTML格式邮件
                        .body(String::from("发送HTML格式失败就只能回退了.")),
                )
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentType::TEXT_HTML)
                        .body(String::from(html)),
                ),
        )
        .expect("构造邮件消息体失败:");

    let creds = Credentials::new(username, password);

    // 默认使用TLS连接
    let mailer = SmtpTransport::relay(smtp_server)
        .expect("解析SMTP服务器失败:")
        .credentials(creds)
        .build();

    match mailer.send(&email) {
        Ok(_) => println!("邮件发送成功!"),
        Err(e) => panic!("发送邮件失败: {e:?}"),
    }
}

这和之前的案例应该区别不大, 不需要额外的讲解。

效果如下:

image-20240203151006522

在邮件中嵌入图片

在邮件中嵌入一个图片就稍稍的复杂一点,这跟邮件的协议有关。

use std::fs;
use lettre::{
    message::{header::{self, Header}, MultiPart, SinglePart},
    SmtpTransport, Message, Transport,
};
use lettre::transport::smtp::authentication::Credentials;

fn main() {
    // 邮件内容
    let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>来自Rust代码发送的邮件</title>
</head>
<body>
    <div style="display: flex; flex-direction: column; align-items: center;">
        <img src="cid:demo.jpg" height="85" width="128" >
        <h2 style="font-family: Arial, Helvetica, sans-serif;">做人嘛, 最重要是开心啦...</h2>
    </div>
    
</body>
</html>"#;
    // 注意上面的demo.jpg没有用<>包裹起来

    // 邮件的参数
    let from_address = "你的邮箱";
    // 大家可以测试的时候可以随便往这个邮箱发....
    let to_address = "[email protected]";
    let subject = "用Rust发一封在内容中嵌入图片的邮件";

    // 认证相关
    let username = String::from("你的邮箱");
    let password = String::from("你的密码");
    let smtp_server = "smtp.126.com";

    // 构造邮件体内容
    let email = Message::builder()
        .from(from_address.parse().expect("解析发送邮箱地址失败:"))
        .to(to_address.parse().expect("解析收件邮箱地址失败: "))
        .subject(subject)
        .multipart(
            MultiPart::alternative() // alternative构造一个可回退的邮件体
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentType::TEXT_PLAIN)
                        // 应该是为了防止邮件服务器不支持HTML格式邮件
                        .body(String::from("发送HTML格式失败就只能回退了.")),
                )
                .multipart(
                MultiPart::related()
                        .singlepart(
                            SinglePart::builder()
                            .header(header::ContentType::TEXT_HTML)
                            .body(String::from(html))
                        )
                        .singlepart(
                            SinglePart::builder()
                            .header(header::ContentType::parse("image/jpeg").expect("解析图片格式失败:"))
                                    .header(header::ContentDisposition::inline_with_name("demo.jpg"))
                                    // 注意demo.jpg用尖括号包裹起来了
                                    .header(header::ContentId::parse("<demo.jpg>").expect("解析成content-id失败:"))
                                    .body(fs::read("demo.jpg").expect("读取文件失败:"))
                        )   
                        ,
                )
        )
        .expect("构造邮件消息体失败:");

    let creds = Credentials::new(username, password);

    // 默认使用TLS连接
    let mailer = SmtpTransport::relay(smtp_server)
        .expect("解析SMTP服务器失败:")
        .credentials(creds)
        .build();

    match mailer.send(&email) {
        Ok(_) => println!("邮件发送成功!"),
        Err(e) => panic!("发送邮件失败: {e:?}"),
    }
}

HTML中嵌入图片要注意的点主要是content-id, 只有设置了这个header才能在html中引用,而引用需要使用cid:demo.jpg这种格式,其中demo.jpgcontent-id, 但是看代码你会发现设置的是<demo.jpg>, 为啥要加个尖括号包裹起来勒? 我认为可能是为了包裹特殊字符吧。

上面发送的代码效果如下:

image-20240203144647864

发送带附件的邮件

发送附件就要比嵌入图片容易很多了,因为提供了对于的类型。

use lettre::{
    message::{header::{self, ContentType}, Attachment, MultiPart, SinglePart},
    SmtpTransport, Message, Transport,
};
use std::fs;
use lettre::transport::smtp::authentication::Credentials;

fn main() {
    // 邮件内容
    let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>来自Rust代码发送的邮件</title>
</head>
<body>
    <div style="display: flex; flex-direction: column; align-items: center;">
        <h2 style="font-family: Arial, Helvetica, sans-serif;">做人嘛, 最重要是开心啦...</h2>
    </div>
</body>
</html>"#;

    // 邮件的附件构造
    let attachment = Attachment::new("demo.jpg".into())
        .body(
            fs::read("demo.jpg").expect("读取照片失败"), 
            ContentType::parse("image/jpeg").expect("解析图片格式失败:")
        );

    // 邮件的参数
    let from_address = "你的邮箱";
    // 大家可以测试的时候可以随便往这个邮箱发....
    let to_address = "[email protected]";
    let subject = "用Rust发一封带附件的邮件";

    // 认证相关
    let username = String::from("你的邮箱");
    let password = String::from("你的密码");
    let smtp_server = "smtp.126.com";

    // 构造邮件体内容
    let email = Message::builder()
        .from(from_address.parse().expect("解析发送邮箱地址失败:"))
        .to(to_address.parse().expect("解析收件邮箱地址失败: "))
        .subject(subject)
        .multipart(
            MultiPart::alternative() // alternative构造一个可回退的邮件体
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentType::TEXT_PLAIN)
                        // 应该是为了防止邮件服务器不支持HTML格式邮件
                        .body(String::from("发送HTML格式失败就只能回退了.")), 
                )
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentType::TEXT_HTML)
                        .body(String::from(html)),
                )
                .singlepart(attachment),
        )
        .expect("构造邮件消息体失败:");

    let creds = Credentials::new(username, password);

    // 默认使用TLS连接
    let mailer = SmtpTransport::relay(smtp_server)
        .expect("解析SMTP服务器失败:")
        .credentials(creds)
        .build();

    match mailer.send(&email) {
        Ok(_) => println!("邮件发送成功!"),
        Err(e) => panic!("发送邮件失败: {e:?}"),
    }
}

总结

不同的程序写邮件其实大同小异,这就跟不同的编程使用http客户端一样,因为协议是一样的,所以很多概念都是差不多的,不同点主要是不同的程序库会提供不同的接口来供用户使用,所以需要查阅相关文档来熟悉接口,但是接口很容易忘记的啦,所以写成文章以备后续自己复制粘贴。

参考链接