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
220KB
4.5K SLoC
new-tokio-smtp
维护状态
该crate目前处于被动维护状态,这意味着
- 我仍然会响应错误,并修复它们(如果这不需要任何 重大 重写)。
- 我仍然会评估和合并PR。 (只要我不被淹没,并且它们不会重写整个crate或类似情况 ;-)
此外,维护状态可能会在未来恢复到主动维护。
描述
new-tokio-smtp crate提供了一个使用tokio的可扩展SMTP(简单邮件传输协议)实现。
此crate仅提供SMTP功能,这意味着它既不提供创建邮件的功能,也不提供例如在接收者临时不可用时重试发送邮件的功能。
虽然它只提供SMTP功能,但它以易于与高级库集成的这种方式编写。互操作性通过两种机制提供
-
SMTP命令的定义允许库用户定义自己的命令,此库提供的所有命令在理论上都可以在外部库中实现,这包括一些更特殊的命令,如
STARTTLS
、EHLO
和DATA
。此外,可以将Connection
转换为Io
实例,这提供了一系列易于实现新命令的有用功能,例如Io.parse_response
。 -
例如,像
Domain
或ClientId
这样的语法结构可以解析,但它们也有“未检查”的构造函数,这允许具有自己验证的库跳过冗余的验证,例如,如果一个邮件库可能提供Mailbox
类型的邮件地址和名称,如果可以实现一个简单的From/
Into
实现,就可以廉价地将其转换为Forward-Path
。 (另外,它们也可以实现自己的Mail
命令,如果这对它们有好处) -
提供的命令(和语法结构)是用一种稳健的方式编写的,例如,允许实现像
SMTPUTF8
这样的扩展。这种方法唯一的缺点是它相信由更高级别的库创建的部分是有效的,例如,它不会验证给它的邮件实际上是否是7位ASCII码,或者它是否不包含“孤儿”'\n'
(或'\r'
)字符。但这是可以接受的,因为这个库是用来通过SMTP发送邮件的,而不是用来创建这样的邮件。(注意,虽然它相信它确实通过检查最后一个EHLO
命令的结果来验证是否可以使用命令,即它不会允许你在不支持它的邮件服务器上发送STARTTLS
命令) -
将逻辑错误(即服务器响应代码为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文件中进行了说明。
可用性助手
该库提供了一些可用性助手
-
chain::chain
提供了一种简单的方法来链式调用多个SMTP命令,当链中的前一个命令未以任何方式失败时,将发送每个命令。 -
mock_support
功能:扩展Socket抽象,不仅抽象了Socket是TcpStream
或TlsStream
,而且还添加了一个新的变体,即boxedMockStream
,这使得smtp库,以及在其之上构建的库更容易进行测试。 -
mock::MockStream
(使用mock-impl
功能)这是一个简单的MockStream
实现,允许您测试发送给它哪些数据,并为其模拟响应。(尽管它目前仅限于一个固定的预定义对话,如果需要更多,必须使用自定义的MockStream
实现) -
future_ext::ResultWithContextExt
:提供ctx_and_then
和ctx_or_else
方法,这使得处理将结果解析为项到(此处为连接)的上下文字元和属于不同抽象级别的Result
(此处为可能的CommandError
,而未来的Error
是一个连接错误,例如破损的管道)变得更容易。
限制/待办事项
如前所述,这个库有一些限制,因为它是为了仅做SMTP,而不做其他任何事情。尽管有一些其他限制,但这些限制可能会在未来版本中得到解决。
-
没有为
send_mail::MailAddress
提供邮件地址解析器,也没有为ForwardPath
/ReversePath
提供解析器(它们可以使用from_unchecked
来构建)。这将在找到一个“仅”处理邮件地址并且正确处理的库时得到解决。 -
没有对扩展状态代码的“内置”支持,这主要是因为我没有时间做这个,以优雅的方式更改这可能需要一些针对
Response
类型的API更改,并且应该在v1.0
之前完成。 -
提供的命令数量目前限制在一个小型但有用的子集中,提供的命令包括
BDAT
和更多种类的AUTH
(目前提供的是PLAIN
和简单的LOGIN
,这对于大多数情况已经足够,但支持例如OAuth2
会更好)。 -
没有对
PIPELINING
的支持,尽管大多数扩展可以使用自定义命令实现,但对于流水线来说并不适用。虽然存在一种在不过度破坏API的情况下实现流水线的方法,但由于时间限制,这目前不是计划中的。 -
目前还没有稳定版本(
v1.0
),因为tokio
还不稳定。当tokio
变得稳定时,应该发布一个稳定版本,但如果稍后实现了PIPELINING
,可能还需要发布另一个版本(尽管在当前实现它的概念中,除了自定义命令的实现者之外,几乎没有破坏性变化)。
文档
文档可以在 docs.rs 上查看。
许可协议
在以下任一许可协议下发布:
- Apache License, Version 2.0, (LICENSE-APACHE 或 http://www.apache.org/licenses/LICENSE-2.0)
- MIT 协议 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非你明确声明,否则所有有意提交以包含在本作品中的贡献,如 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_mails
和connect_send_quit
现在接受一个IntoIterable
而不是流- 发送时需要所有值都准备好了,所以
Stream
不是很合适 - 这也意味着你现在可以传递一个
Vec
或一个std::iter:once
- 发送时需要所有值都准备好了,所以
GeneralError
现在不再是PreviousRequestKilledConnection
错误变体,而是一个std::io::Error::new(std::io::ErrorKind::NoConnection, "...")
返回,这使得它更容易被使用它的库适应,并且语义上与以前的解决方案兼容得更好send_all_mails
和connect_send_quit
现在返回一个流,而不是解析为流的未来
-
v0.7.1
- 更新了依赖项,确保 tokio 不产生弃用警告,因为导入已移动到 tokio 的不同位置
-
v0.8.0
- 更新
tokio-tls
/native-tls
到 v0.2.x - 将创建构建器的函数从
build*
重命名为builder*
- 更新
-
v0.8.1
- 添加了
SelectCmd
和EitherCmd
- 添加了
-
v0.8.2
- 修复了对于
EsmtpValue
错误解析的错误 - 现在使用
rustfmt
。 - 对于不可解析的 ehlo 功能响应行,发出警告但不失败
- 修复了对于
-
v0.9.0
- 进行了一些小的 API 清理。
- 制作了
log
和(默认)功能。所以如果没有设置日志实现者,就不需要编译这个包。 - 如果错误的 EHLO 功能响应行应该触发语法错误或被跳过(可能记录错误的关键字/值),则可以进行配置。
- 添加了跟踪级别日志,记录整个服务器对话(除邮件正文和密码外)。
-
v0.9.1
- 跟踪日志记录 smtp 连接建立到的套接字地址(或尝试建立连接失败)。
贡献者
- katyo (https://github.com/katyo)
依赖关系
~4–14MB
~147K SLoC