9个版本

0.9.1 2020年6月20日
0.9.0 2020年6月6日
0.8.2 2020年5月31日
0.8.1 2018年11月6日
0.6.1 2018年8月22日

#11 in #smtp-server


3 个crate中使用(2个直接使用)

MIT/Apache

220KB
4.5K SLoC

new-tokio-smtp docs License License

维护状态

该crate目前处于被动维护状态,这意味着

  • 我仍然会响应错误,并修复它们(如果这不需要任何 重大 重写)。
  • 我仍然会评估和合并PR。 (只要我不被淹没,并且它们不会重写整个crate或类似情况 ;-)

此外,维护状态可能会在未来恢复到主动维护。

描述

new-tokio-smtp crate提供了一个使用tokio的可扩展SMTP(简单邮件传输协议)实现。

此crate仅提供SMTP功能,这意味着它既不提供创建邮件的功能,也不提供例如在接收者临时不可用时重试发送邮件的功能。

虽然它只提供SMTP功能,但它以易于与高级库集成的这种方式编写。互操作性通过两种机制提供

  1. SMTP命令的定义允许库用户定义自己的命令,此库提供的所有命令在理论上都可以在外部库中实现,这包括一些更特殊的命令,如STARTTLSEHLODATA。此外,可以将Connection转换为Io实例,这提供了一系列易于实现新命令的有用功能,例如Io.parse_response

  2. 例如,像DomainClientId这样的语法结构可以解析,但它们也有“未检查”的构造函数,这允许具有自己验证的库跳过冗余的验证,例如,如果一个邮件库可能提供Mailbox类型的邮件地址和名称,如果可以实现一个简单的From/Into实现,就可以廉价地将其转换为Forward-Path。 (另外,它们也可以实现自己的Mail命令,如果这对它们有好处)

  3. 提供的命令(和语法结构)是用一种稳健的方式编写的,例如,允许实现像SMTPUTF8这样的扩展。这种方法唯一的缺点是它相信由更高级别的库创建的部分是有效的,例如,它不会验证给它的邮件实际上是否是7位ASCII码,或者它是否不包含“孤儿”'\n'(或'\r')字符。但这是可以接受的,因为这个库是用来通过SMTP发送邮件的,而不是用来创建这样的邮件。(注意,虽然它相信它确实通过检查最后一个EHLO命令的结果来验证是否可以使用命令,即它不会允许你在不支持它的邮件服务器上发送STARTTLS命令)

  4. 将逻辑错误(即服务器响应代码为550)与更致命的错误(例如,管道损坏)分开处理

示例

extern crate futures;
extern crate tokio;
extern crate new_tokio_smtp;
#[macro_use]
extern crate vec1;
extern crate rpassword;

use std::io::{stdin, stdout, Write};

use futures::stream::Stream;
use futures::future::lazy;
use new_tokio_smtp::error::GeneralError;
use new_tokio_smtp::{command, Connection, ConnectionConfig, Domain};
use new_tokio_smtp::send_mail::{
    Mail, EncodingRequirement,
    MailAddress, MailEnvelop,
};

struct Request {
    config: ConnectionConfig<command::auth::Plain>,
    mails: Vec<MailEnvelop>
}

fn main() {
    let Request { config, mails } = read_request();
    // We only have iter map overhead because we
    // don't have a failable mail encoding step, which normally is required.
    let mails = mails.into_iter().map(|m| -> Result<_, GeneralError> { Ok(m) });

    println!("[now starting tokio]");
    tokio::run(lazy(move || {
        println!("[start connect_send_quit]");
        Connection::connect_send_quit(config, mails)
            //Stream::for_each is design wise broken in futures v0.1
            .then(|result| Ok(result))
            .for_each(|result| {
                if let Err(err) = result {
                    println!("[sending mail failed]: {}", err);
                } else {
                    println!("[successfully send mail]")
                }
                Ok(())
            })
    }))
}


fn read_request() -> Request {

    println!("preparing to send mail with ethereal.email");
    let sender = read_email();
    let passwd = read_password();

    // The `from_unchecked` will turn into a `.parse()` in the future.
    let config = ConnectionConfig
        ::builder(Domain::from_unchecked("smtp.ethereal.email"))
            .expect("resolving domain failed")
        .auth(command::auth::Plain::from_username(sender.clone(), passwd)
            .expect("username/password can not contain \\0 bytes"))
        .build();

    // the from_unchecked normally can be used if we know the address is valid
    // a mail address parser will be added at some point in the future
    let send_to = MailAddress::from_unchecked("[email protected]");

    // using string fmt to crate mails IS A
    // REALLY BAD IDEA there are a ton of ways
    // this can go wrong, so don't do this in
    // practice, use some library to crate mails
    let raw_mail = format!(concat!(
        "Date: Thu, 14 Jun 2018 11:22:18 +0000\r\n",
        "From: You <{}>\r\n",
        //ethereal doesn't delivers any mail so it's fine
        "To: Invalid <{}>\r\n",
        "Subject: I am spam?\r\n",
        "\r\n",
        "...\r\n"
    ), sender.as_str(), send_to.as_str());

    // this normally adapts to a higher level abstraction
    // of mail then this crate provides
    let mail_data = Mail::new(EncodingRequirement::None, raw_mail.to_owned());

    let mail = MailEnvelop::new(sender, vec1![ send_to ], mail_data);

    Request {
        config,
        mails: vec![ mail ]
    }
}

fn read_email() -> MailAddress {
    let stdout = stdout();
    let mut handle = stdout.lock();
    write!(handle, "enter ethereal.email mail address\n[Note mail is not validated in this example]: ")
        .unwrap();
    handle.flush().unwrap();

    let mut line = String::new();
    stdin().read_line(&mut line).unwrap();
    MailAddress::from_unchecked(line.trim())
}

fn read_password() -> String {
    rpassword::prompt_password_stdout("password: ").unwrap()
}

测试

cargo测试 --功能 "模拟实现"

现在仅运行cargo test不起作用,这可能在将来通过cargo支持“仅测试默认功能”或类似的功能得到修复。

调试SMTP

如果启用了日志(默认)功能,并将日志级别设置为跟踪,则记录整个客户端/服务器交互。

服务器发送的任何行在接收后并解析前都会被记录,客户端发送的任何行在发送前都会被记录(在刷新前)。

例外情况是发送邮件正文不会记录。此外,对于客户端发送的任何以“AUTH”开头的行,除了下一个单词之外的所有内容都将被红字覆盖,以防止记录密码、访问令牌等。例如,在auth plain登录的情况下,只有"AUTH PLAIN <redacted>"会被记录。

这意味着在使用跟踪日志时,以下内容仍将被记录

  • 客户端ID
  • 服务器ID
  • 服务器问候消息(可能包含连接到的服务器的客户端IP或DNS名称,取决于您连接到的服务器)。
  • 发送的邮件地址
  • 所有接收的邮件地址

鉴于跟踪日志仅应启用用于调试目的,因此即使与GDPR不兼容也不会有问题。如果您仍然设置它,使其不适用于此crate。例如,使用env_logger,它可能类似于`RUST_LOG="new_tokio_smtp=warn,trace"`以启用除smtp之外的所有位置的跟踪日志。但您可能会遇到GDPR不兼容性,因为这可能会记录例如连接客户端的IP地址等。

请注意,跟踪日志确实意味着在写入日志之外还有性能开销,因为跟踪日志是在低级别上完成的,其中不是字符串而是字节被处理,因此它们必须在每个命令行(不是邮件消息)发送之前被转换回字符串,并且客户端发送的每个命令行都需要检查是否以AUTH开头,并需要进行红字覆盖等。

概念

库背后的概念在notes/concept.md文件中进行了说明。

可用性助手

该库提供了一些可用性助手

  1. chain::chain提供了一种简单的方法来链式调用多个SMTP命令,当链中的前一个命令未以任何方式失败时,将发送每个命令。

  2. mock_support功能:扩展Socket抽象,不仅抽象了Socket是TcpStreamTlsStream,而且还添加了一个新的变体,即boxed MockStream,这使得smtp库,以及在其之上构建的库更容易进行测试。

  3. mock::MockStream(使用mock-impl功能)这是一个简单的MockStream实现,允许您测试发送给它哪些数据,并为其模拟响应。(尽管它目前仅限于一个固定的预定义对话,如果需要更多,必须使用自定义的MockStream实现)

  4. future_ext::ResultWithContextExt:提供ctx_and_thenctx_or_else方法,这使得处理将结果解析为项到(此处为连接)的上下文字元和属于不同抽象级别的Result(此处为可能的CommandError,而未来的Error是一个连接错误,例如破损的管道)变得更容易。

限制/待办事项

如前所述,这个库有一些限制,因为它是为了做SMTP,而不做其他任何事情。尽管有一些其他限制,但这些限制可能会在未来版本中得到解决。

  1. 没有为send_mail::MailAddress提供邮件地址解析器,也没有为ForwardPath/ReversePath提供解析器(它们可以使用from_unchecked来构建)。这将在找到一个“仅”处理邮件地址并且正确处理的库时得到解决。

  2. 没有对扩展状态代码的“内置”支持,这主要是因为我没有时间做这个,以优雅的方式更改这可能需要一些针对Response类型的API更改,并且应该在v1.0之前完成。

  3. 提供的命令数量目前限制在一个小型但有用的子集中,提供的命令包括BDAT和更多种类的AUTH(目前提供的是PLAIN和简单的LOGIN,这对于大多数情况已经足够,但支持例如OAuth2会更好)。

  4. 没有对PIPELINING的支持,尽管大多数扩展可以使用自定义命令实现,但对于流水线来说并不适用。虽然存在一种在不过度破坏API的情况下实现流水线的方法,但由于时间限制,这目前不是计划中的。

  5. 目前还没有稳定版本(v1.0),因为tokio还不稳定。当tokio变得稳定时,应该发布一个稳定版本,但如果稍后实现了PIPELINING,可能还需要发布另一个版本(尽管在当前实现它的概念中,除了自定义命令的实现者之外,几乎没有破坏性变化)。

文档

文档可以在 docs.rs 上查看。

许可协议

在以下任一许可协议下发布:

任选其一。

贡献

除非你明确声明,否则所有有意提交以包含在本作品中的贡献,如 Apache-2.0 许可证中定义,均应双许可,如上所述,无任何附加条款或条件。

变更日志

  • v0.4:

    • from_str_unchecked 重命名为 from_unchecked
    • Cmd.exec 现在接受 Io 而不是 Connection
      • CmdFuture 替换为 ExecFuture
      • Connection.send_simple_cmd 现在是 Io.exec_simple_cmd
  • v0.5:

    • 改进了 ClientId
      • ClientIdentity 重命名为 ClientId
      • 添加了 hostname() 构造函数
    • 添加了 ConnectionConfig 的构建器
      • 移除了旧的 with_ 构造函数
    • 将所有 Auth* 命令放入一个 auth 模块中(例如 AuthPlain => auth::plain::Plain
    • 更改了功能命名模式
  • v0.6

    • 添加了本地非安全连接的连接构建器
    • 为构建器添加了它们所构建的类型构造函数
    • auth::plain::NullCodePoint 重命名为 auth::plain::NullCodePointError
  • v0.7

    • send_all_mailsconnect_send_quit 现在接受一个 IntoIterable 而不是流
      • 发送时需要所有值都准备好了,所以 Stream 不是很合适
      • 这也意味着你现在可以传递一个 Vec 或一个 std::iter:once
    • GeneralError 现在不再是 PreviousRequestKilledConnection 错误变体,而是一个 std::io::Error::new(std::io::ErrorKind::NoConnection, "...") 返回,这使得它更容易被使用它的库适应,并且语义上与以前的解决方案兼容得更好
    • send_all_mailsconnect_send_quit 现在返回一个流,而不是解析为流的未来
  • v0.7.1

    • 更新了依赖项,确保 tokio 不产生弃用警告,因为导入已移动到 tokio 的不同位置
  • v0.8.0

    • 更新 tokio-tls/native-tls 到 v0.2.x
    • 将创建构建器的函数从 build* 重命名为 builder*
  • v0.8.1

    • 添加了 SelectCmdEitherCmd
  • v0.8.2

    • 修复了对于 EsmtpValue 错误解析的错误
    • 现在使用 rustfmt
    • 对于不可解析的 ehlo 功能响应行,发出警告但不失败
  • v0.9.0

    • 进行了一些小的 API 清理。
    • 制作了 log 和(默认)功能。所以如果没有设置日志实现者,就不需要编译这个包。
    • 如果错误的 EHLO 功能响应行应该触发语法错误或被跳过(可能记录错误的关键字/值),则可以进行配置。
    • 添加了跟踪级别日志,记录整个服务器对话(除邮件正文和密码外)。
  • v0.9.1

    • 跟踪日志记录 smtp 连接建立到的套接字地址(或尝试建立连接失败)。

贡献者

依赖关系

~4–14MB
~147K SLoC