Various changes, mainly on data types
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
#![crate_id = "client"]
|
||||
|
||||
extern crate smtp;
|
||||
use std::io::net::tcp::TcpStream;
|
||||
use smtp::client::SmtpClient;
|
||||
use std::strbuf::StrBuf;
|
||||
|
||||
fn main() {
|
||||
let mut email_client: SmtpClient<TcpStream> = SmtpClient::new("localhost", None, None);
|
||||
email_client.send_mail("user@localhost", [&"user@localhost"], "Test email");
|
||||
let mut email_client: SmtpClient<StrBuf, TcpStream> = SmtpClient::new(StrBuf::from_str("localhost"), None, None);
|
||||
email_client.send_mail(StrBuf::from_str("<user@localhost>"), vec!(StrBuf::from_str("<user@localhost>")), StrBuf::from_str("Test email"));
|
||||
}
|
||||
|
||||
@@ -1,58 +1,55 @@
|
||||
/*!
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
Simple SMTP client.
|
||||
|
||||
# Usage
|
||||
|
||||
```
|
||||
let mut email_client: SmtpClient<TcpStream> = SmtpClient::new("localhost", None, None);
|
||||
email_client.send_mail("user@example.org", [&"user@example.com"], "Example email");
|
||||
```
|
||||
|
||||
*/
|
||||
/*! A simple SMTP client */
|
||||
|
||||
use std::fmt;
|
||||
use std::from_str;
|
||||
use std::fmt::{Show, Formatter};
|
||||
use std::from_str::FromStr;
|
||||
use std::str::from_utf8;
|
||||
use std::result::Result;
|
||||
use std::io::{IoResult, IoError};
|
||||
use std::strbuf::StrBuf;
|
||||
use std::io::{IoResult, Reader, Writer};
|
||||
use std::io::net::ip::{SocketAddr, Port};
|
||||
use std::io::net::tcp::TcpStream;
|
||||
use std::io::net::addrinfo::get_host_addresses;
|
||||
use common::{SMTP_PORT, CRLF, get_first_word};
|
||||
use common::{CRLF, get_first_word};
|
||||
use commands;
|
||||
use commands::{Command, SmtpCommand, EhloKeyword};
|
||||
|
||||
// Define smtp_fail! and smtp_success!
|
||||
use commands::{SMTP_PORT, SmtpCommand, EsmtpParameter};
|
||||
|
||||
/// Contains an SMTP reply, with separed code and message
|
||||
#[deriving(Eq,Clone)]
|
||||
pub struct SmtpResponse {
|
||||
/// Server respinse code code
|
||||
pub struct SmtpResponse<T> {
|
||||
/// Server response code
|
||||
code: uint,
|
||||
/// Server response string
|
||||
message: ~str
|
||||
message: T
|
||||
}
|
||||
|
||||
impl fmt::Show for SmtpResponse {
|
||||
/// Format SMTP response display
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> {
|
||||
impl<T: Show> Show for SmtpResponse<T> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.buf.write(
|
||||
format!("{} {}", self.code.to_str(), self.message).as_bytes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl from_str::FromStr for SmtpResponse {
|
||||
/// Parse an SMTP response line
|
||||
fn from_str(s: &str) -> Option<SmtpResponse> {
|
||||
// FromStr ?
|
||||
impl FromStr for SmtpResponse<StrBuf> {
|
||||
fn from_str(s: &str) -> Option<SmtpResponse<StrBuf>> {
|
||||
if s.len() < 5 {
|
||||
None
|
||||
} else {
|
||||
if [" ", "-"].contains(&s.slice(3,4)) {
|
||||
if vec!(" ", "-").contains(&s.slice(3,4)) {
|
||||
Some(SmtpResponse{
|
||||
code: from_str(s.slice_to(3)).unwrap(),
|
||||
message: s.slice_from(4).to_owned()
|
||||
message: StrBuf::from_str(s.slice_from(4))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -61,36 +58,43 @@ impl from_str::FromStr for SmtpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpResponse {
|
||||
/// Check the response code
|
||||
fn with_code(&self, expected_codes: &[uint]) -> Result<SmtpResponse,SmtpResponse> {
|
||||
let response = SmtpResponse{code: self.code, message: self.message.clone()};
|
||||
for &code in expected_codes.iter() {
|
||||
if code == self.code {
|
||||
return Ok(response);
|
||||
}
|
||||
impl<T: Clone> SmtpResponse<T> {
|
||||
/// Checks the response code
|
||||
fn with_code(&self, expected_codes: Vec<uint>) -> Result<SmtpResponse<T>,SmtpResponse<T>> {
|
||||
let response = self.clone();
|
||||
if expected_codes.contains(&self.code) {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(response)
|
||||
}
|
||||
return Err(response);
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about an SMTP server
|
||||
#[deriving(Eq,Clone)]
|
||||
pub struct SmtpServerInfo {
|
||||
pub struct SmtpServerInfo<T> {
|
||||
/// Server name
|
||||
name: ~str,
|
||||
name: T,
|
||||
/// ESMTP features supported by the server
|
||||
esmtp_features: Option<~[EhloKeyword]>
|
||||
esmtp_features: Option<Vec<EsmtpParameter>>
|
||||
}
|
||||
|
||||
impl SmtpServerInfo {
|
||||
/// Parse supported ESMTP features
|
||||
fn parse_esmtp_response(message: &str) -> Option<~[EhloKeyword]> {
|
||||
let mut esmtp_features: ~[EhloKeyword] = ~[];
|
||||
for line in message.split_str(CRLF) {
|
||||
match from_str::<SmtpResponse>(line) {
|
||||
impl<T: Show> Show for SmtpServerInfo<T>{
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.buf.write(
|
||||
format!("{} with {}", self.name, self.esmtp_features).as_bytes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Str> SmtpServerInfo<T> {
|
||||
/// Parses supported ESMTP features
|
||||
fn parse_esmtp_response(message: T) -> Option<Vec<EsmtpParameter>> {
|
||||
let mut esmtp_features = Vec::new();
|
||||
for line in message.into_owned().split_str(CRLF) {
|
||||
match from_str::<SmtpResponse<StrBuf>>(line) {
|
||||
Some(SmtpResponse{code: 250, message: message}) => {
|
||||
match from_str::<EhloKeyword>(message) {
|
||||
match from_str::<EsmtpParameter>(message.into_owned()) {
|
||||
Some(keyword) => esmtp_features.push(keyword),
|
||||
None => ()
|
||||
}
|
||||
@@ -99,13 +103,13 @@ impl SmtpServerInfo {
|
||||
}
|
||||
}
|
||||
match esmtp_features.len() {
|
||||
0 => None,
|
||||
_ => Some(esmtp_features)
|
||||
0 => None,
|
||||
_ => Some(esmtp_features)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
fn supports_feature(&self, keyword: EhloKeyword) -> bool {
|
||||
fn supports_feature(&self, keyword: EsmtpParameter) -> bool {
|
||||
match self.esmtp_features.clone() {
|
||||
Some(esmtp_features) => {
|
||||
esmtp_features.contains(&keyword)
|
||||
@@ -115,195 +119,368 @@ impl SmtpServerInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Show for SmtpServerInfo {
|
||||
/// Format SMTP server information display
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> {
|
||||
f.buf.write(
|
||||
format!("{:s} with {}", self.name, self.esmtp_features).as_bytes()
|
||||
)
|
||||
}
|
||||
/// Contains the state of the current transaction
|
||||
#[deriving(Eq,Clone)]
|
||||
pub enum SmtpClientState {
|
||||
/// The server is unconnected
|
||||
Unconnected,
|
||||
/// The connection and banner were successful
|
||||
Connected,
|
||||
/// An HELO or EHLO was successfully sent
|
||||
HeloSent,
|
||||
/// A MAIL command was successful
|
||||
MailSent,
|
||||
/// At least one RCPT command was sucessful
|
||||
RcptSent,
|
||||
/// A DATA command was successful
|
||||
DataSent
|
||||
}
|
||||
|
||||
macro_rules! check_state_in(
|
||||
($expected_states:expr) => (
|
||||
if ! $expected_states.contains(&self.state) {
|
||||
fail!("Wrong transaction state for this command.");
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
macro_rules! check_state_not_in(
|
||||
($expected_states:expr) => (
|
||||
if $expected_states.contains(&self.state) {
|
||||
fail!("Wrong transaction state for this command.");
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
macro_rules! smtp_fail_if_err(
|
||||
($response:expr) => (
|
||||
match $response {
|
||||
Err(response) => {
|
||||
self.smtp_fail(response)
|
||||
},
|
||||
Ok(..) => {}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
/// Structure that implements a simple SMTP client
|
||||
pub struct SmtpClient<S> {
|
||||
pub struct SmtpClient<T, S> {
|
||||
/// TCP stream between client and server
|
||||
stream: Option<S>,
|
||||
/// Host we are connecting to
|
||||
host: ~str,
|
||||
host: T,
|
||||
/// Port we are connecting on
|
||||
port: Port,
|
||||
/// Our hostname for HELO/EHLO commands
|
||||
my_hostname: ~str,
|
||||
my_hostname: T,
|
||||
/// Information about the server
|
||||
server_info: Option<SmtpServerInfo>
|
||||
/// Value is None before HELO/EHLO
|
||||
server_info: Option<SmtpServerInfo<T>>,
|
||||
/// Transaction state, permits to check order againt RFCs
|
||||
state: SmtpClientState
|
||||
}
|
||||
|
||||
impl<S> SmtpClient<S> {
|
||||
/// Create a new SMTP client
|
||||
pub fn new(host: &str, port: Option<Port>, my_hostname: Option<&str>) -> SmtpClient<S> {
|
||||
impl<S> SmtpClient<StrBuf, S> {
|
||||
/// Creates a new SMTP client
|
||||
pub fn new(host: StrBuf, port: Option<Port>, my_hostname: Option<StrBuf>) -> SmtpClient<StrBuf, S> {
|
||||
SmtpClient{
|
||||
stream: None,
|
||||
host: host.to_owned(),
|
||||
host: host,
|
||||
port: port.unwrap_or(SMTP_PORT),
|
||||
my_hostname: my_hostname.unwrap_or("localhost").to_owned(),
|
||||
server_info: None
|
||||
my_hostname: my_hostname.unwrap_or(StrBuf::from_str("localhost")),
|
||||
server_info: None,
|
||||
state: Unconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpClient<TcpStream> {
|
||||
/// Send an SMTP command
|
||||
pub fn send_command(&mut self, command: Command, option: Option<~str>) -> SmtpResponse {
|
||||
self.send_and_get_response(SmtpCommand::new(command, option).to_str())
|
||||
impl SmtpClient<StrBuf, TcpStream> {
|
||||
/// Connects to the configured server
|
||||
pub fn connect(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
if !self.stream.is_none() {
|
||||
fail!("The connection is already established");
|
||||
}
|
||||
let ip = match get_host_addresses(self.host.clone().into_owned()) {
|
||||
Ok(ip_vector) => ip_vector[0], // TODO : select a random ip
|
||||
Err(..) => fail!("Cannot resolve {:s}", self.host)
|
||||
};
|
||||
self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) {
|
||||
Ok(stream) => Some(stream),
|
||||
Err(..) => fail!("Cannot connect to {:s}:{:u}", self.host, self.port)
|
||||
};
|
||||
match self.get_reply() {
|
||||
Some(response) => match response.with_code(vec!(220)) {
|
||||
Ok(response) => {
|
||||
self.state = Connected;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
},
|
||||
None => fail!("No banner on {}", self.host)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sends an email
|
||||
pub fn send_mail(&mut self, from_address: StrBuf, to_addresses: Vec<StrBuf>, message: StrBuf) {
|
||||
let my_hostname = self.my_hostname.clone();
|
||||
|
||||
// Connect
|
||||
match self.connect() {
|
||||
Ok(..) => {},
|
||||
Err(response) => fail!("Cannot connect to {:s}:{:u}. Server says: {}", self.host, self.port, response)
|
||||
}
|
||||
|
||||
// Extended Hello or Hello
|
||||
match self.ehlo(my_hostname.clone()) {
|
||||
Err(SmtpResponse{code: 550, message: _}) => {
|
||||
smtp_fail_if_err!(self.helo(my_hostname))
|
||||
},
|
||||
Err(response) => {
|
||||
self.smtp_fail(response)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
info!("SMTP server: {:s}", self.server_info.clone().unwrap().to_str());
|
||||
|
||||
// Checks message encoding according to the server's capability
|
||||
// TODO : Add an encoding check.
|
||||
if ! self.server_info.clone().unwrap().supports_feature(commands::EightBitMime) {
|
||||
if false {
|
||||
self.smtp_fail("Server does not accepts UTF-8 strings");
|
||||
}
|
||||
}
|
||||
|
||||
// Mail
|
||||
smtp_fail_if_err!(self.mail(from_address, None));
|
||||
|
||||
// Recipient
|
||||
// TODO Return rejected addresses
|
||||
for to_address in to_addresses.iter() {
|
||||
smtp_fail_if_err!(self.rcpt(to_address.clone(), None));
|
||||
}
|
||||
|
||||
// Data
|
||||
smtp_fail_if_err!(self.data());
|
||||
|
||||
// Message content
|
||||
smtp_fail_if_err!(self.message(message));
|
||||
|
||||
// Quit
|
||||
smtp_fail_if_err!(self.quit());
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Writer + Reader + Clone> SmtpClient<StrBuf, S> {
|
||||
/// Sends an SMTP command
|
||||
pub fn send_command(&mut self, command: SmtpCommand<StrBuf>) -> SmtpResponse<StrBuf> {
|
||||
self.send_and_get_response(format!("{}", command))
|
||||
}
|
||||
|
||||
/// Send an email
|
||||
pub fn send_message(&mut self, message: ~str) -> SmtpResponse {
|
||||
self.send_and_get_response(format!("{:s}{:s}.", message, CRLF))
|
||||
/// Sends an email
|
||||
pub fn send_message(&mut self, message: StrBuf) -> SmtpResponse<StrBuf> {
|
||||
self.send_and_get_response(format!("{}{:s}.", message, CRLF))
|
||||
}
|
||||
|
||||
/// Send a complete message or a command to the server and get the response
|
||||
fn send_and_get_response(&mut self, string: ~str) -> SmtpResponse {
|
||||
/// Sends a complete message or a command to the server and get the response
|
||||
fn send_and_get_response(&mut self, string: ~str) -> SmtpResponse<StrBuf> {
|
||||
match (&mut self.stream.clone().unwrap() as &mut Writer)
|
||||
.write_str(format!("{:s}{:s}", string, CRLF)) {
|
||||
Ok(..) => debug!("Write success"),
|
||||
Ok(..) => debug!("Wrote: {:s}", string),
|
||||
Err(..) => fail!("Could not write to stream")
|
||||
}
|
||||
|
||||
match self.get_reply() {
|
||||
Some(response) => response,
|
||||
None => fail!("No answer on {}", self.host)
|
||||
Some(response) => {debug!("Read: {:s}", response.to_str()); response},
|
||||
None => fail!("No answer on {:s}", self.host)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the SMTP response
|
||||
fn get_reply(&mut self) -> Option<SmtpResponse> {
|
||||
/// Gets the SMTP response
|
||||
fn get_reply(&mut self) -> Option<SmtpResponse<StrBuf>> {
|
||||
let response = match self.read_to_str() {
|
||||
Ok(string) => string,
|
||||
Err(..) => fail!("No answer")
|
||||
};
|
||||
|
||||
from_str::<SmtpResponse>(response)
|
||||
from_str::<SmtpResponse<StrBuf>>(response)
|
||||
}
|
||||
|
||||
/// Connect to the configured server
|
||||
pub fn connect(&mut self) -> SmtpResponse {
|
||||
if !self.stream.is_none() {
|
||||
fail!("The connection is already established");
|
||||
}
|
||||
let ip = match get_host_addresses(self.host.clone()) {
|
||||
Ok(ip_vector) => ip_vector[0],
|
||||
Err(..) => fail!("Cannot resolve {}", self.host)
|
||||
};
|
||||
self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) {
|
||||
Ok(stream) => Some(stream),
|
||||
Err(..) => fail!("Cannot connect to {}:{}", self.host, self.port)
|
||||
};
|
||||
match self.get_reply() {
|
||||
Some(response) => response,
|
||||
None => fail!("No banner on {}", self.host)
|
||||
/// Closes the connection and fail with a given messgage
|
||||
fn smtp_fail<T: Show>(&mut self, reason: T) {
|
||||
if self.is_connected() {
|
||||
match self.quit() {
|
||||
Ok(..) => {},
|
||||
Err(response) => fail!("Failed: {}", response)
|
||||
}
|
||||
}
|
||||
self.close();
|
||||
fail!("Failed: {}", reason);
|
||||
}
|
||||
|
||||
/// Print an SMTP response as info
|
||||
fn smtp_success(&mut self, response: SmtpResponse) {
|
||||
info!("{:u} {:s}", response.code, response.message);
|
||||
/// Checks if the server is connected
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.noop().is_ok()
|
||||
}
|
||||
|
||||
/// Send a QUIT command and end the program
|
||||
fn smtp_fail(&mut self, command: ~str, reason: &str) {
|
||||
self.send_command(commands::Quit, None);
|
||||
fail!("{} failed: {:s}", command, reason);
|
||||
|
||||
/// Closes the TCP stream
|
||||
pub fn close(&mut self) {
|
||||
drop(self.stream.clone().unwrap());
|
||||
}
|
||||
|
||||
/// Send an email
|
||||
pub fn send_mail(&mut self, from_addr: &str, to_addrs: &[&str], message: &str) {
|
||||
let my_hostname = self.my_hostname.clone();
|
||||
|
||||
// Connect
|
||||
match self.connect().with_code([220]) {
|
||||
Ok(response) => self.smtp_success(response),
|
||||
Err(response) => self.smtp_fail(~"CONNECT", response.to_str())
|
||||
}
|
||||
|
||||
// Extended Hello or Hello
|
||||
match self.send_command(commands::Ehlo, Some(my_hostname.clone())).with_code([250, 500]) {
|
||||
Ok(SmtpResponse{code: 250, message: message}) => {
|
||||
|
||||
/// Send a HELO command
|
||||
pub fn helo(&mut self, my_hostname: StrBuf) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_in!([Connected]);
|
||||
|
||||
match self.send_command(commands::Hello(my_hostname.clone())).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.server_info = Some(
|
||||
SmtpServerInfo{
|
||||
name: get_first_word(message.clone()),
|
||||
esmtp_features: SmtpServerInfo::parse_esmtp_response(message.clone())
|
||||
name: get_first_word(response.message.clone()),
|
||||
esmtp_features: None
|
||||
}
|
||||
);
|
||||
self.smtp_success(SmtpResponse{code: 250u, message: message});
|
||||
self.state = HeloSent;
|
||||
Ok(response)
|
||||
},
|
||||
Ok(..) => {
|
||||
match self.send_command(commands::Helo, Some(my_hostname.clone())).with_code([250]) {
|
||||
Ok(response) => {
|
||||
self.server_info = Some(
|
||||
SmtpServerInfo{
|
||||
name: get_first_word(response.message.clone()),
|
||||
esmtp_features: None
|
||||
}
|
||||
);
|
||||
self.smtp_success(response);
|
||||
},
|
||||
Err(response) => self.smtp_fail(~"HELO", response.to_str())
|
||||
Err(response) => Err(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sends a EHLO command
|
||||
pub fn ehlo(&mut self, my_hostname: StrBuf) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_not_in!([Unconnected]);
|
||||
|
||||
match self.send_command(commands::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.server_info = Some(
|
||||
SmtpServerInfo{
|
||||
name: get_first_word(response.message.clone()),
|
||||
esmtp_features: SmtpServerInfo::parse_esmtp_response(response.message.clone())
|
||||
}
|
||||
);
|
||||
self.state = HeloSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => Err(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a MAIL command
|
||||
pub fn mail(&mut self, from_address: StrBuf, options: Option<StrBuf>) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_in!([HeloSent]);
|
||||
|
||||
match self.send_command(commands::Mail(from_address, options)).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.state = MailSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a RCPT command
|
||||
pub fn rcpt(&mut self, to_address: StrBuf, options: Option<StrBuf>) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_in!([MailSent, RcptSent]);
|
||||
|
||||
match self.send_command(commands::Recipient(to_address, options)).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.state = RcptSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a DATA command
|
||||
pub fn data(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_in!([RcptSent]);
|
||||
|
||||
match self.send_command(commands::Data).with_code(vec!(354)) {
|
||||
Ok(response) => {
|
||||
self.state = DataSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message_content: StrBuf) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_in!([DataSent]);
|
||||
|
||||
match self.send_message(message_content).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.state = HeloSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sends a QUIT command
|
||||
pub fn quit(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_not_in!([Unconnected]);
|
||||
match self.send_command(commands::Quit).with_code(vec!(221)) {
|
||||
Ok(response) => {
|
||||
self.close();
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a RSET command
|
||||
pub fn rset(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_not_in!([Unconnected]);
|
||||
match self.send_command(commands::Reset).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
if vec!(MailSent, RcptSent, DataSent).contains(&self.state) {
|
||||
self.state = HeloSent;
|
||||
}
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => self.smtp_fail(~"EHLO", response.to_str())
|
||||
}
|
||||
|
||||
debug!("SMTP server : {:s}", self.server_info.clone().unwrap().to_str())
|
||||
|
||||
// Check message encoding according to the server's capability
|
||||
if ! self.server_info.clone().unwrap().supports_feature(commands::EightBitMime) {
|
||||
if ! message.is_ascii() {
|
||||
self.smtp_fail(~"DATA", "Server does not accepts UTF-8 strings")
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Mail
|
||||
match self.send_command(commands::Mail, Some(from_addr.to_owned())).with_code([250]) {
|
||||
Ok(response) => self.smtp_success(response),
|
||||
Err(response) => self.smtp_fail(~"MAIL", response.to_str())
|
||||
}
|
||||
|
||||
// Recipient
|
||||
for &to_addr in to_addrs.iter() {
|
||||
match self.send_command(commands::Rcpt, Some(to_addr.to_owned())).with_code([250]) {
|
||||
Ok(response) => self.smtp_success(response),
|
||||
Err(response) => self.smtp_fail(~"RCPT", response.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
match self.send_command(commands::Data, None).with_code([354]) {
|
||||
Ok(response) => self.smtp_success(response),
|
||||
Err(response) => self.smtp_fail(~"DATA", response.to_str())
|
||||
}
|
||||
|
||||
// Message content
|
||||
match self.send_message(message.to_owned()).with_code([250]) {
|
||||
Ok(response) => self.smtp_success(response),
|
||||
Err(response) => self.smtp_fail(~"MESSAGE", response.to_str())
|
||||
}
|
||||
|
||||
// Quit
|
||||
match self.send_command(commands::Quit, None).with_code([221]) {
|
||||
Ok(response) => self.smtp_success(response),
|
||||
Err(response) => self.smtp_fail(~"DATA", response.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a NOOP commands
|
||||
pub fn noop(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_not_in!([Unconnected]);
|
||||
self.send_command(commands::Noop).with_code(vec!(250))
|
||||
}
|
||||
|
||||
/// Sends a VRFY command
|
||||
pub fn vrfy(&mut self, to_address: StrBuf) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
||||
check_state_not_in!([Unconnected]);
|
||||
self.send_command(commands::Verify(to_address)).with_code(vec!(250))
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader for SmtpClient<TcpStream> {
|
||||
/// Read a string from the client socket
|
||||
impl<T, S: Reader + Clone> Reader for SmtpClient<T, S> {
|
||||
/// Reads a string from the client socket
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
||||
self.stream.clone().unwrap().read(buf)
|
||||
}
|
||||
|
||||
/// Read a string from the client socket
|
||||
/// Reads a string from the client socket
|
||||
fn read_to_str(&mut self) -> IoResult<~str> {
|
||||
let mut buf = [0u8, ..1000];
|
||||
|
||||
@@ -311,21 +488,19 @@ impl Reader for SmtpClient<TcpStream> {
|
||||
Ok(bytes_read) => from_utf8(buf.slice_to(bytes_read - 1)).unwrap(),
|
||||
Err(..) => fail!("Read error")
|
||||
};
|
||||
debug!("Read: {:s}", response);
|
||||
|
||||
return Ok(response.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer for SmtpClient<TcpStream> {
|
||||
/// Send a string on the client socket
|
||||
impl<T, S: Writer + Clone> Writer for SmtpClient<T, S> {
|
||||
/// Sends a string on the client socket
|
||||
fn write(&mut self, buf: &[u8]) -> IoResult<()> {
|
||||
self.stream.clone().unwrap().write(buf)
|
||||
}
|
||||
|
||||
/// Send a string on the client socket
|
||||
/// Sends a string on the client socket
|
||||
fn write_str(&mut self, string: &str) -> IoResult<()> {
|
||||
debug!("Wrote: {:s}", string);
|
||||
self.stream.clone().unwrap().write_str(string)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +1,99 @@
|
||||
/*!
|
||||
* SMTP commands and ESMTP features library
|
||||
*
|
||||
* RFC 5321 : https://tools.ietf.org/html/rfc5321#section-4.1
|
||||
*/
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::from_str;
|
||||
use std::io::IoError;
|
||||
/*! SMTP commands [1] and ESMTP features [2] library
|
||||
|
||||
/// List of SMTP commands
|
||||
[1] https://tools.ietf.org/html/rfc5321#section-4.1
|
||||
[2] http://tools.ietf.org/html/rfc1869
|
||||
|
||||
*/
|
||||
|
||||
use std::fmt::{Show, Formatter, Result};
|
||||
use std::io::net::ip::Port;
|
||||
use std::from_str::FromStr;
|
||||
|
||||
/// Default SMTP port
|
||||
pub static SMTP_PORT: Port = 25;
|
||||
//pub static SMTPS_PORT: Port = 465;
|
||||
//pub static SUBMISSION_PORT: Port = 587;
|
||||
|
||||
/// SMTP commands
|
||||
/// We do not implement the following SMTP commands, as they were deprecated in RFC 5321
|
||||
/// and must not be used by clients :
|
||||
/// SEND, SOML, SAML, TURN
|
||||
#[deriving(Eq,Clone)]
|
||||
pub enum Command {
|
||||
pub enum SmtpCommand<T> {
|
||||
/// Extended Hello command
|
||||
Ehlo,
|
||||
ExtendedHello(T),
|
||||
/// Hello command
|
||||
Helo,
|
||||
/// Mail command
|
||||
Mail,
|
||||
/// Recipient command
|
||||
Rcpt,
|
||||
Hello(T),
|
||||
/// Mail command, takes optionnal options
|
||||
Mail(T, Option<T>),
|
||||
/// Recipient command, takes optionnal options
|
||||
Recipient(T, Option<T>),
|
||||
/// Data command
|
||||
Data,
|
||||
/// Reset command
|
||||
Rset,
|
||||
/// Send command, deprecated in RFC 5321
|
||||
Send,
|
||||
/// Send Or Mail command, deprecated in RFC 5321
|
||||
Soml,
|
||||
/// Send And Mail command, deprecated in RFC 5321
|
||||
Saml,
|
||||
Reset,
|
||||
/// Verify command
|
||||
Vrfy,
|
||||
Verify(T),
|
||||
/// Expand command
|
||||
Expn,
|
||||
/// Help command
|
||||
Help,
|
||||
Expand(T),
|
||||
/// Help command, takes optionnal options
|
||||
Help(Option<T>),
|
||||
/// Noop command
|
||||
Noop,
|
||||
/// Quit command
|
||||
Quit,
|
||||
/// Turn command, deprecated in RFC 5321
|
||||
Turn,
|
||||
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Tell if the command accetps an string argument.
|
||||
pub fn takes_argument(&self) -> bool{
|
||||
match *self {
|
||||
Ehlo => true,
|
||||
Helo => true,
|
||||
Mail => true,
|
||||
Rcpt => true,
|
||||
Data => false,
|
||||
Rset => false,
|
||||
Send => true,
|
||||
Soml => true,
|
||||
Saml => true,
|
||||
Vrfy => true,
|
||||
Expn => true,
|
||||
Help => true,
|
||||
Noop => false,
|
||||
Quit => false,
|
||||
Turn => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell if an argument is needed by the command.
|
||||
pub fn needs_argument(&self) -> bool {
|
||||
match *self {
|
||||
Ehlo => true,
|
||||
Helo => true,
|
||||
Mail => true,
|
||||
Rcpt => true,
|
||||
Data => false,
|
||||
Rset => false,
|
||||
Send => true,
|
||||
Soml => true,
|
||||
Saml => true,
|
||||
Vrfy => true,
|
||||
Expn => true,
|
||||
Help => false,
|
||||
Noop => false,
|
||||
Quit => false,
|
||||
Turn => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Show for Command {
|
||||
/// Format SMTP command display
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> {
|
||||
impl<T: Show> Show for SmtpCommand<T> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
f.buf.write(match *self {
|
||||
Ehlo => "EHLO",
|
||||
Helo => "Helo",
|
||||
Mail => "MAIL FROM:",
|
||||
Rcpt => "RCPT TO:",
|
||||
Data => "DATA",
|
||||
Rset => "RSET",
|
||||
Send => "SEND TO:",
|
||||
Soml => "SOML TO:",
|
||||
Saml => "SAML TO:",
|
||||
Vrfy => "VRFY",
|
||||
Expn => "EXPN",
|
||||
Help => "HELP",
|
||||
Noop => "NOOP",
|
||||
Quit => "QUIT",
|
||||
Turn => "TURN"
|
||||
ExtendedHello(ref my_hostname) =>
|
||||
format!("EHLO {}", my_hostname.clone()),
|
||||
Hello(ref my_hostname) =>
|
||||
format!("HELO {}", my_hostname.clone()),
|
||||
Mail(ref from_address, None) =>
|
||||
format!("MAIL FROM:{}", from_address.clone()),
|
||||
Mail(ref from_address, Some(ref options)) =>
|
||||
format!("MAIL FROM:{} {}", from_address.clone(), options.clone()),
|
||||
Recipient(ref to_address, None) =>
|
||||
format!("RCPT TO:{}", to_address.clone()),
|
||||
Recipient(ref to_address, Some(ref options)) =>
|
||||
format!("RCPT TO:{} {}", to_address.clone(), options.clone()),
|
||||
Data => ~"DATA",
|
||||
Reset => ~"RSET",
|
||||
Verify(ref address) =>
|
||||
format!("VRFY {}", address.clone()),
|
||||
Expand(ref address) =>
|
||||
format!("EXPN {}", address.clone()),
|
||||
Help(None) => ~"HELP",
|
||||
Help(Some(ref argument)) =>
|
||||
format!("HELP {}", argument.clone()),
|
||||
Noop => ~"NOOP",
|
||||
Quit => ~"QUIT",
|
||||
}.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure for a complete SMTP command, containing an optionnal string argument.
|
||||
pub struct SmtpCommand {
|
||||
/// The SMTP command (e.g. MAIL, QUIT, ...)
|
||||
command: Command,
|
||||
/// An optionnal argument to the command
|
||||
argument: Option<~str>
|
||||
}
|
||||
|
||||
impl SmtpCommand {
|
||||
/// Return a new structure from the name of the command and an optionnal argument.
|
||||
pub fn new(command: Command, argument: Option<~str>) -> SmtpCommand {
|
||||
match (command.takes_argument(), command.needs_argument(), argument.clone()) {
|
||||
(true, true, None) => fail!("Wrong SMTP syntax : argument needed"),
|
||||
(false, false, Some(x)) => fail!("Wrong SMTP syntax : {:s} not accepted", x),
|
||||
_ => SmtpCommand {command: command, argument: argument}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Show for SmtpCommand {
|
||||
/// Return the formatted command, ready to be used in an SMTP session.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> {
|
||||
f.buf.write(
|
||||
match (self.command.takes_argument(), self.command.needs_argument(), self.argument.clone()) {
|
||||
(true, _, Some(argument)) => format!("{} {}", self.command, argument),
|
||||
(_, false, None) => format!("{}", self.command),
|
||||
_ => fail!("Wrong SMTP syntax")
|
||||
}.as_bytes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[deriving(Eq,Clone)]
|
||||
pub enum EhloKeyword {
|
||||
pub enum EsmtpParameter {
|
||||
/// 8BITMIME keyword
|
||||
/// RFC 6152 : https://tools.ietf.org/html/rfc6152
|
||||
EightBitMime,
|
||||
}
|
||||
|
||||
impl fmt::Show for EhloKeyword {
|
||||
/// Format SMTP response display
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> {
|
||||
impl Show for EsmtpParameter {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
f.buf.write(
|
||||
match self {
|
||||
&EightBitMime => "8BITMIME".as_bytes()
|
||||
@@ -162,10 +102,9 @@ impl fmt::Show for EhloKeyword {
|
||||
}
|
||||
}
|
||||
|
||||
impl from_str::FromStr for EhloKeyword {
|
||||
// Match keywords
|
||||
fn from_str(s: &str) -> Option<EhloKeyword> {
|
||||
match s {
|
||||
impl FromStr for EsmtpParameter {
|
||||
fn from_str(s: &str) -> Option<EsmtpParameter> {
|
||||
match s.as_slice() {
|
||||
"8BITMIME" => Some(EightBitMime),
|
||||
_ => None
|
||||
}
|
||||
@@ -174,42 +113,22 @@ impl from_str::FromStr for EhloKeyword {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{SmtpCommand, EhloKeyword};
|
||||
|
||||
#[test]
|
||||
fn test_command_parameters() {
|
||||
assert!((super::Help).takes_argument() == true);
|
||||
assert!((super::Rset).takes_argument() == false);
|
||||
assert!((super::Helo).needs_argument() == true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_to_str() {
|
||||
assert!(super::Turn.to_str() == ~"TURN");
|
||||
}
|
||||
use super::{EsmtpParameter};
|
||||
|
||||
#[test]
|
||||
fn test_command_fmt() {
|
||||
assert!(format!("{}", super::Turn) == ~"TURN");
|
||||
//assert!(format!("{}", super::Noop) == ~"NOOP");
|
||||
assert!(format!("{}", super::ExtendedHello("me")) == ~"EHLO me");
|
||||
assert!(format!("{}", super::Mail("test", Some("option"))) == ~"MAIL FROM:test option");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_simple_command() {
|
||||
assert!(SmtpCommand::new(super::Turn, None).to_str() == ~"TURN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_argument_command() {
|
||||
assert!(SmtpCommand::new(super::Ehlo, Some(~"example.example")).to_str() == ~"EHLO example.example");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ehlokeyword_fmt() {
|
||||
fn test_esmtp_parameter_fmt() {
|
||||
assert!(format!("{}", super::EightBitMime) == ~"8BITMIME");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ehlokeyword_from_str() {
|
||||
assert!(from_str::<EhloKeyword>("8BITMIME") == Some(super::EightBitMime));
|
||||
assert!(from_str::<EsmtpParameter>("8BITMIME") == Some(super::EightBitMime));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
/*!
|
||||
* Common definitions for SMTP
|
||||
*
|
||||
* Needs to be organized later.
|
||||
*/
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
use std::io::net::ip::Port;
|
||||
/*! Common definitions for SMTP
|
||||
|
||||
/// Default SMTP port
|
||||
pub static SMTP_PORT: Port = 25;
|
||||
//pub static SMTPS_PORT: Port = 465;
|
||||
//pub static SUBMISSION_PORT: Port = 587;
|
||||
Needs to be organized later.
|
||||
|
||||
/// End of SMTP commands
|
||||
*/
|
||||
|
||||
use std::strbuf::StrBuf;
|
||||
|
||||
pub static SP: &'static str = " ";
|
||||
pub static CRLF: &'static str = "\r\n";
|
||||
|
||||
/// Add quotes to emails
|
||||
/// Adds quotes to emails
|
||||
pub fn quote_email_address(addr: &str) -> ~str {
|
||||
match (addr.slice_to(1), addr.slice_from(addr.len()-1)) {
|
||||
("<", ">") => addr.to_owned(),
|
||||
@@ -22,7 +26,7 @@ pub fn quote_email_address(addr: &str) -> ~str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove quotes from emails
|
||||
/// Removes quotes from emails
|
||||
pub fn unquote_email_address(addr: &str) -> ~str {
|
||||
match (addr.slice_to(1), addr.slice_from(addr.len() - 1)) {
|
||||
("<", ">") => addr.slice(1, addr.len() - 1).to_owned(),
|
||||
@@ -31,8 +35,8 @@ pub fn unquote_email_address(addr: &str) -> ~str {
|
||||
}
|
||||
|
||||
/// Returns the first word of a string, or the string if it contains no space
|
||||
pub fn get_first_word(string: &str) -> ~str {
|
||||
string.split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap().to_owned()
|
||||
pub fn get_first_word<T: Str>(string: T) -> StrBuf {
|
||||
StrBuf::from_str(string.into_owned().split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -47,12 +51,13 @@ mod test {
|
||||
fn test_unquote_email_address() {
|
||||
assert!(super::unquote_email_address("<plop>") == ~"plop");
|
||||
assert!(super::unquote_email_address("plop") == ~"plop");
|
||||
assert!(super::unquote_email_address("<plop") == ~"<plop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_first_word() {
|
||||
assert!(super::get_first_word("first word") == ~"first");
|
||||
assert!(super::get_first_word("first word\ntest") == ~"first");
|
||||
assert!(super::get_first_word("first") == ~"first");
|
||||
assert!(super::get_first_word("first word") == StrBuf::from_str("first"));
|
||||
assert!(super::get_first_word("first word\r\ntest") == StrBuf::from_str("first"));
|
||||
assert!(super::get_first_word("first") == StrBuf::from_str("first"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
/*!
|
||||
* SMTP library
|
||||
*
|
||||
* For now, contains only a basic and uncomplete SMTP client and some common general functions.
|
||||
*/
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
/*! SMTP library
|
||||
|
||||
This library implements a simple SMTP client.
|
||||
RFC 5321 : https://tools.ietf.org/html/rfc5321#section-4.1
|
||||
|
||||
It does NOT manages email content.
|
||||
|
||||
It also implements the following extesnions
|
||||
8BITMIME (RFC 6152 : https://tools.ietf.org/html/rfc6152)
|
||||
|
||||
# Usage
|
||||
|
||||
```
|
||||
let mut email_client: SmtpClient<StrBuf, TcpStream> = SmtpClient::new(StrBuf::from_str("localhost"), None, None);
|
||||
email_client.send_mail(StrBuf::from_str("<user@example.com>"), vec!(StrBuf::from_str("<user@example.org>")), StrBuf::from_str("Test email"));
|
||||
```
|
||||
|
||||
# TODO:
|
||||
Add SSL/TLS
|
||||
Add AUTH
|
||||
|
||||
*/
|
||||
|
||||
#![crate_id = "smtp#0.1-pre"]
|
||||
|
||||
#![comment = "Rust SMTP client"]
|
||||
#![desc = "Rust SMTP client"]
|
||||
#![comment = "Simple SMTP client"]
|
||||
#![license = "ASL2"]
|
||||
#![crate_type = "lib"]
|
||||
|
||||
//#[crate_type = "dylib"];
|
||||
//#[crate_type = "rlib"];
|
||||
#![doc(html_root_url = "http://www.rust-ci.org/amousset/rust-smtp/doc/")]
|
||||
|
||||
#![feature(macro_rules)]
|
||||
#![deny(non_camel_case_types)]
|
||||
#![deny(missing_doc)]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user