Add client code and reorganize sources
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,2 @@
|
||||
/target/
|
||||
/doc/
|
||||
/Cargo.lock
|
||||
*~
|
||||
*.swp
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
language: rust
|
||||
env:
|
||||
global:
|
||||
- secure: Lk4jMRKDf9NDdCP82mlGODiaBbImPVuerT71+whL/k0zrYJQjQMROWhKTuOWngZW4EvVjgoNNk18LWjIVneAFQkruuMln459CKuUa3BBmVwNkkjPOxqD+3zzFbnubdIlgkMHf2CgnbrXt5Pwa0wI5qwaz4p0bj78YT//7eG6lDM=
|
||||
after_script:
|
||||
- cargo doc
|
||||
- curl http://www.rust-ci.org/artifacts/put?t=$RUSTCI_TOKEN | sh
|
||||
|
||||
@@ -2,10 +2,4 @@
|
||||
|
||||
name = "smtp"
|
||||
version = "0.0.1-pre"
|
||||
#readme = "README.md"
|
||||
authors = ["Alexis Mousset <contact@amousset.eu>"]
|
||||
#tags = ["smtp", "email", "library"]
|
||||
|
||||
[lib]
|
||||
name = "smtpcommon"
|
||||
path = "src/lib.rs"
|
||||
|
||||
18
README.md
18
README.md
@@ -11,16 +11,20 @@ This library is designed for Rust 0.13.0-nightly (master).
|
||||
Install
|
||||
-------
|
||||
|
||||
Use Cargo to build this library.
|
||||
If you're using `Cargo`, just add this to your `Cargo.toml`:
|
||||
|
||||
cargo build
|
||||
```toml
|
||||
[dependencies.smtp]
|
||||
|
||||
Todo
|
||||
----
|
||||
git = "https://github.com/amousset/rust-smtp.git"
|
||||
```
|
||||
|
||||
- RFC compliance
|
||||
- SSL/TLS support
|
||||
- AUTH support
|
||||
Otherwise, just clone this repo and run `cargo build`.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The documentation is available on [GitHub pages](amousset.github.io/rust-smtp/smtp/).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
110
examples/client.rs
Normal file
110
examples/client.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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.
|
||||
|
||||
extern crate smtp;
|
||||
extern crate getopts;
|
||||
use std::io::stdin;
|
||||
use std::io::net::tcp::TcpStream;
|
||||
use std::string::String;
|
||||
use std::io::net::ip::Port;
|
||||
use std::os;
|
||||
use smtp::client::SmtpClient;
|
||||
use getopts::{optopt,optflag,getopts,OptGroup,usage};
|
||||
|
||||
fn sendmail(source_address: String, recipient_addresses: Vec<String>, message: String, server: String, port: Option<Port>, my_hostname: Option<String>) {
|
||||
let mut email_client: SmtpClient<TcpStream> =
|
||||
SmtpClient::new(
|
||||
server,
|
||||
port,
|
||||
my_hostname);
|
||||
email_client.send_mail::<TcpStream>(
|
||||
source_address,
|
||||
recipient_addresses,
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
fn print_usage(description: String, _opts: &[OptGroup]) {
|
||||
println!("{}", usage(description.as_slice(), _opts));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = os::args();
|
||||
|
||||
let mut args_string = Vec::new();
|
||||
for arg in args.iter() {
|
||||
args_string.push(arg.clone());
|
||||
};
|
||||
|
||||
let program = args[0].clone();
|
||||
let description = format!("Usage: {0} [options...] recipients\n\n\
|
||||
This program reads a message on standard input until it reaches EOF,\
|
||||
then tries to send it using the given paramters.\n\n\
|
||||
Example: {0} -r user@example.org user@example.com < message.txt", program);
|
||||
|
||||
let opts = [
|
||||
optopt("r", "reverse-path", "set the sender address", "FROM_ADDRESS"),
|
||||
optopt("p", "port", "set the port to use, default is 25", "PORT"),
|
||||
optopt("s", "server", "set the server to use, default is localhost", "SERVER"),
|
||||
optopt("m", "my-hostname", "set the hostname used by the client", "MY_HOSTNAME"),
|
||||
optflag("h", "help", "print this help menu"),
|
||||
optflag("v", "verbose", "display the transaction details"),
|
||||
];
|
||||
let matches = match getopts(args_string.tail(), opts) {
|
||||
Ok(m) => { m }
|
||||
Err(f) => { fail!("{}", f) }
|
||||
};
|
||||
if matches.opt_present("h") {
|
||||
print_usage(description, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let sender = match matches.opt_str("r") {
|
||||
Some(sender) => sender,
|
||||
None => {
|
||||
println!("The sender option is required");
|
||||
print_usage(program, opts);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let server = match matches.opt_str("s") {
|
||||
Some(server) => server,
|
||||
None => String::from_str("localhost")
|
||||
};
|
||||
|
||||
let my_hostname = match matches.opt_str("m") {
|
||||
Some(my_hostname) => Some(my_hostname),
|
||||
None => None
|
||||
};
|
||||
|
||||
let port = match matches.opt_str("p") {
|
||||
Some(port) => from_str::<Port>(port.as_slice()),
|
||||
None => None
|
||||
|
||||
};
|
||||
|
||||
let recipients_str: &str = if !matches.free.is_empty() {
|
||||
matches.free[0].as_slice()
|
||||
} else {
|
||||
print_usage(description, opts);
|
||||
return;
|
||||
};
|
||||
let mut recipients = Vec::new();
|
||||
for recipient in recipients_str.split(' ') {
|
||||
recipients.push(String::from_str(recipient))
|
||||
}
|
||||
|
||||
let mut message = String::new();
|
||||
for line in stdin().lines() {
|
||||
message.push_str(line.unwrap().as_slice());
|
||||
}
|
||||
|
||||
sendmail(sender, recipients, message, server, port, my_hostname);
|
||||
}
|
||||
38
src/client/connecter.rs
Normal file
38
src/client/connecter.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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.
|
||||
|
||||
// Taken fron rust-http
|
||||
|
||||
//! TODO
|
||||
|
||||
use std::io::IoResult;
|
||||
use std::io::net::ip::SocketAddr;
|
||||
use std::io::net::tcp::TcpStream;
|
||||
|
||||
/// A trait for the concept of opening a stream connected to a IP socket address.
|
||||
///
|
||||
/// Why is this here? So that we can implement things which must make
|
||||
/// connections in terms of *anything* that can make such a connection rather
|
||||
/// than in terms of `TcpStream` only. This is handy for testing and for SSL.
|
||||
pub trait Connecter {
|
||||
/// TODO
|
||||
fn connect(host: &str, port: u16) -> IoResult<Self>;
|
||||
/// TODO
|
||||
fn peer_name(&mut self) -> IoResult<SocketAddr>;
|
||||
}
|
||||
|
||||
impl Connecter for TcpStream {
|
||||
fn connect(host: &str, port: u16) -> IoResult<TcpStream> {
|
||||
TcpStream::connect(host, port)
|
||||
}
|
||||
|
||||
fn peer_name(&mut self) -> IoResult<SocketAddr> {
|
||||
self.peer_name()
|
||||
}
|
||||
}
|
||||
514
src/client/mod.rs
Normal file
514
src/client/mod.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
// 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 client
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{Show, Formatter};
|
||||
use std::str::from_utf8;
|
||||
use std::result::Result;
|
||||
use std::string::String;
|
||||
use std::io::{IoResult, Reader, Writer};
|
||||
use std::io::net::ip::Port;
|
||||
|
||||
use common::{get_first_word, unquote_email_address};
|
||||
use response::SmtpResponse;
|
||||
use extension;
|
||||
use extension::SmtpExtension;
|
||||
use command;
|
||||
use command::SmtpCommand;
|
||||
use common::{SMTP_PORT, CRLF};
|
||||
use transaction;
|
||||
use transaction::TransactionState;
|
||||
|
||||
use client::connecter::Connecter;
|
||||
|
||||
pub mod connecter;
|
||||
|
||||
/// Information about an SMTP server
|
||||
#[deriving(Clone)]
|
||||
struct SmtpServerInfo {
|
||||
/// Server name
|
||||
name: String,
|
||||
/// ESMTP features supported by the server
|
||||
esmtp_features: Option<Vec<SmtpExtension>>
|
||||
}
|
||||
|
||||
impl Show for SmtpServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write(
|
||||
format!("{} with {}",
|
||||
self.name,
|
||||
match self.esmtp_features.clone() {
|
||||
Some(features) => features.to_string(),
|
||||
None => format!("no supported features")
|
||||
}
|
||||
).as_bytes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpServerInfo {
|
||||
/// Parses supported ESMTP features
|
||||
///
|
||||
/// TODO: Improve parsing
|
||||
fn parse_esmtp_response(message: String) -> Option<Vec<SmtpExtension>> {
|
||||
let mut esmtp_features = Vec::new();
|
||||
for line in message.as_slice().split_str(CRLF) {
|
||||
match from_str::<SmtpResponse>(line) {
|
||||
Some(SmtpResponse{code: 250, message: message}) => {
|
||||
match from_str::<SmtpExtension>(message.unwrap().as_slice()) {
|
||||
Some(keyword) => esmtp_features.push(keyword),
|
||||
None => ()
|
||||
}
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
match esmtp_features.len() {
|
||||
0 => None,
|
||||
_ => Some(esmtp_features)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
fn supports_feature(&self, keyword: SmtpExtension) -> Result<SmtpExtension, ()> {
|
||||
match self.esmtp_features.clone() {
|
||||
Some(esmtp_features) => {
|
||||
for feature in esmtp_features.iter() {
|
||||
if keyword.same_extension_as(*feature) {
|
||||
return Ok(*feature);
|
||||
}
|
||||
}
|
||||
Err({})
|
||||
},
|
||||
None => Err({})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct SmtpClient<S> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<S>,
|
||||
/// Host we are connecting to
|
||||
host: String,
|
||||
/// Port we are connecting on
|
||||
port: Port,
|
||||
/// Our hostname for HELO/EHLO commands
|
||||
my_hostname: String,
|
||||
/// Information about the server
|
||||
/// Value is None before HELO/EHLO
|
||||
server_info: Option<SmtpServerInfo>,
|
||||
/// Transaction state, to check the sequence of commands
|
||||
state: TransactionState
|
||||
}
|
||||
|
||||
impl<S> SmtpClient<S> {
|
||||
/// Creates a new SMTP client
|
||||
pub fn new(host: String, port: Option<Port>, my_hostname: Option<String>) -> SmtpClient<S> {
|
||||
SmtpClient{
|
||||
stream: None,
|
||||
host: host,
|
||||
port: port.unwrap_or(SMTP_PORT),
|
||||
my_hostname: my_hostname.unwrap_or(String::from_str("localhost")),
|
||||
server_info: None,
|
||||
state: transaction::Unconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// T : String ou String, selon le support
|
||||
impl<S: Connecter + Reader + Writer + Clone> SmtpClient<S> {
|
||||
|
||||
/// TODO
|
||||
fn smtp_fail_if_err<S>(&mut self, response: Result<SmtpResponse, SmtpResponse>) {
|
||||
match response {
|
||||
Err(response) => {
|
||||
self.smtp_fail::<S, SmtpResponse>(response)
|
||||
},
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect(&mut self) -> Result<SmtpResponse, SmtpResponse> {
|
||||
// connect should not be called when the client is already connected
|
||||
if !self.stream.is_none() {
|
||||
fail!("The connection is already established");
|
||||
}
|
||||
|
||||
// Try to connect
|
||||
self.stream = match Connecter::connect(self.host.clone().as_slice(), self.port) {
|
||||
Ok(stream) => Some(stream),
|
||||
Err(..) => fail!("Cannot connect to the server")
|
||||
};
|
||||
|
||||
// Log the connection
|
||||
info!("Connection established to {}[{}]:{}", self.host, self.stream.clone().unwrap().peer_name().unwrap().ip, self.port);
|
||||
|
||||
match self.get_reply() {
|
||||
Some(response) => match response.with_code(vec!(220)) {
|
||||
Ok(response) => {
|
||||
self.state = transaction::Connected;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
},
|
||||
None => fail!("No banner on {}", self.host)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an email
|
||||
pub fn send_mail<S>(&mut self, from_address: String, to_addresses: Vec<String>, message: String) {
|
||||
let my_hostname = self.my_hostname.clone();
|
||||
let mut smtp_result: Result<SmtpResponse, SmtpResponse>;
|
||||
|
||||
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::<S>(my_hostname.clone().to_string()) {
|
||||
Err(SmtpResponse{code: 550, message: _}) => {
|
||||
smtp_result = self.helo::<S>(my_hostname.clone());
|
||||
self.smtp_fail_if_err::<S>(smtp_result);
|
||||
},
|
||||
Err(response) => {
|
||||
self.smtp_fail::<S, SmtpResponse>(response)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
debug!("Server {}", self.server_info.clone().unwrap());
|
||||
|
||||
// Checks message encoding according to the server's capability
|
||||
// TODO : Add an encoding check.
|
||||
if ! self.server_info.clone().unwrap().supports_feature(extension::EightBitMime).is_ok() {
|
||||
if ! message.clone().to_string().is_ascii() {
|
||||
self.smtp_fail::<S, &str>("Server does not accepts UTF-8 strings");
|
||||
}
|
||||
}
|
||||
|
||||
// Mail
|
||||
smtp_result = self.mail::<S>(from_address.clone(), None);
|
||||
self.smtp_fail_if_err::<S>(smtp_result);
|
||||
|
||||
// Log the mail command
|
||||
info!("from=<{}>, size={}, nrcpt={}", from_address, 42u, to_addresses.len());
|
||||
|
||||
// Recipient
|
||||
// TODO Return rejected addresses
|
||||
// TODO Manage the number of recipients
|
||||
for to_address in to_addresses.iter() {
|
||||
smtp_result = self.rcpt::<S>(to_address.clone(), None);
|
||||
self.smtp_fail_if_err::<S>(smtp_result);
|
||||
}
|
||||
|
||||
// Data
|
||||
smtp_result = self.data::<S>();
|
||||
self.smtp_fail_if_err::<S>(smtp_result);
|
||||
|
||||
// Message content
|
||||
let sent = self.message::<S>(message);
|
||||
|
||||
if sent.clone().is_err() {
|
||||
self.smtp_fail::<S, SmtpResponse>(sent.clone().err().unwrap())
|
||||
}
|
||||
|
||||
info!("to=<{}>, status=sent ({})", to_addresses.clone().connect(">, to=<"), sent.clone().ok().unwrap());
|
||||
|
||||
// Quit
|
||||
smtp_result = self.quit::<S>();
|
||||
self.smtp_fail_if_err::<S>(smtp_result);
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
// TODO : ensure this is an ASCII string
|
||||
fn send_command(&mut self, command: SmtpCommand) -> SmtpResponse {
|
||||
if !self.state.is_command_possible(command.clone()) {
|
||||
fail!("Bad command sequence");
|
||||
}
|
||||
self.send_and_get_response(format!("{}", command).as_slice())
|
||||
}
|
||||
|
||||
/// Sends an email
|
||||
fn send_message(&mut self, message: String) -> SmtpResponse {
|
||||
self.send_and_get_response(format!("{}{:s}.", message, CRLF).as_slice())
|
||||
}
|
||||
|
||||
/// Sends a complete message or a command to the server and get the response
|
||||
fn send_and_get_response(&mut self, string: &str) -> SmtpResponse {
|
||||
match (&mut self.stream.clone().unwrap() as &mut Writer)
|
||||
.write_str(format!("{:s}{:s}", string, CRLF).as_slice()) { // TODO improve this
|
||||
Ok(..) => debug!("Wrote: {:s}", string),
|
||||
Err(..) => fail!("Could not write to stream")
|
||||
}
|
||||
|
||||
match self.get_reply() {
|
||||
Some(response) => {debug!("Read: {}", response); response},
|
||||
None => fail!("No answer on {:s}", self.host)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn get_reply(&mut self) -> Option<SmtpResponse> {
|
||||
let response = match self.read_to_string() {
|
||||
Ok(string) => string,
|
||||
Err(..) => fail!("No answer")
|
||||
};
|
||||
from_str::<SmtpResponse>(response.as_slice())
|
||||
}
|
||||
|
||||
/// Closes the connection and fail with a given messgage
|
||||
fn smtp_fail<S, T: Show>(&mut self, reason: T) {
|
||||
let is_connected = self.is_connected::<S>();
|
||||
if is_connected {
|
||||
match self.quit::<S>() {
|
||||
Ok(..) => {},
|
||||
Err(response) => fail!("Failed: {}", response)
|
||||
}
|
||||
}
|
||||
self.close();
|
||||
fail!("Failed: {}", reason);
|
||||
}
|
||||
|
||||
/// Checks if the server is connected
|
||||
pub fn is_connected<S>(&mut self) -> bool {
|
||||
self.noop::<S>().is_ok()
|
||||
}
|
||||
|
||||
/// Closes the TCP stream
|
||||
pub fn close(&mut self) {
|
||||
// Close the TCP connection
|
||||
drop(self.stream.clone().unwrap());
|
||||
// Reset client state
|
||||
self.stream = None;
|
||||
self.state = transaction::Unconnected;
|
||||
self.server_info = None;
|
||||
}
|
||||
|
||||
/// Send a HELO command
|
||||
pub fn helo<S>(&mut self, my_hostname: String) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::Hello(my_hostname.clone())).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.server_info = Some(
|
||||
SmtpServerInfo{
|
||||
name: get_first_word(response.message.clone().unwrap()),
|
||||
esmtp_features: None
|
||||
}
|
||||
);
|
||||
self.state = transaction::HelloSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => Err(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a EHLO command
|
||||
pub fn ehlo<S>(&mut self, my_hostname: String) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.server_info = Some(
|
||||
SmtpServerInfo{
|
||||
name: get_first_word(response.message.clone().unwrap()),
|
||||
esmtp_features: SmtpServerInfo::parse_esmtp_response(response.message.clone().unwrap())
|
||||
}
|
||||
);
|
||||
self.state = transaction::HelloSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => Err(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a MAIL command
|
||||
pub fn mail<S>(&mut self, from_address: String, options: Option<Vec<String>>) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::Mail(unquote_email_address(from_address.to_string()), options)).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.state = transaction::MailSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a RCPT command
|
||||
pub fn rcpt<S>(&mut self, to_address: String, options: Option<Vec<String>>) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::Recipient(unquote_email_address(to_address.to_string()), options)).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.state = transaction::RecipientSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a DATA command
|
||||
pub fn data<S>(&mut self) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::Data).with_code(vec!(354)) {
|
||||
Ok(response) => {
|
||||
self.state = transaction::DataSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message<S>(&mut self, message_content: String) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_message(message_content).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
self.state = transaction::HelloSent;
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a QUIT command
|
||||
pub fn quit<S>(&mut self) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::Quit).with_code(vec!(221)) {
|
||||
Ok(response) => {
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a RSET command
|
||||
pub fn rset<S>(&mut self) -> Result<SmtpResponse, SmtpResponse> {
|
||||
match self.send_command(command::Reset).with_code(vec!(250)) {
|
||||
Ok(response) => {
|
||||
if vec!(transaction::MailSent, transaction::RecipientSent, transaction::DataSent).contains(&self.state) {
|
||||
self.state = transaction::HelloSent;
|
||||
}
|
||||
Ok(response)
|
||||
},
|
||||
Err(response) => {
|
||||
Err(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a NOOP commands
|
||||
pub fn noop<S>(&mut self) -> Result<SmtpResponse, SmtpResponse> {
|
||||
self.send_command(command::Noop).with_code(vec!(250))
|
||||
}
|
||||
|
||||
/// Sends a VRFY command
|
||||
pub fn vrfy<S, T>(&mut self, to_address: String) -> Result<SmtpResponse, SmtpResponse> {
|
||||
self.send_command(command::Verify(to_address, None)).with_code(vec!(250))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Reader + Clone> Reader for SmtpClient<S> {
|
||||
/// Reads a string from the client socket
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
||||
self.stream.clone().unwrap().read(buf)
|
||||
}
|
||||
|
||||
/// Reads a string from the client socket
|
||||
// TODO: Size of response ?.
|
||||
fn read_to_string(&mut self) -> IoResult<String> {
|
||||
let mut buf = [0u8, ..1034];
|
||||
|
||||
let response = match self.read(buf) {
|
||||
Ok(bytes_read) => from_utf8(buf.slice_to(bytes_read - 1)).unwrap(),
|
||||
Err(..) => fail!("Read error")
|
||||
};
|
||||
|
||||
return Ok(response.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Writer + Clone> Writer for SmtpClient<S> {
|
||||
/// Sends a string on the client socket
|
||||
fn write(&mut self, buf: &[u8]) -> IoResult<()> {
|
||||
self.stream.clone().unwrap().write(buf)
|
||||
}
|
||||
|
||||
/// Sends a string on the client socket
|
||||
fn write_str(&mut self, string: &str) -> IoResult<()> {
|
||||
self.stream.clone().unwrap().write_str(string)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SmtpServerInfo;
|
||||
use extension;
|
||||
|
||||
#[test]
|
||||
fn test_smtp_server_info_fmt() {
|
||||
assert_eq!(format!("{}", SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: Some(vec!(extension::EightBitMime))
|
||||
}), "name with [8BITMIME]".to_string());
|
||||
assert_eq!(format!("{}", SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: Some(vec!(extension::EightBitMime, extension::Size(42)))
|
||||
}), "name with [8BITMIME, SIZE=42]".to_string());
|
||||
assert_eq!(format!("{}", SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: None
|
||||
}), "name with no supported features".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smtp_server_info_parse_esmtp_response() {
|
||||
assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-8BITMIME\r\n250 SIZE 42")),
|
||||
Some(vec!(extension::EightBitMime, extension::Size(42))));
|
||||
assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-8BITMIME\r\n250 UNKNON 42")),
|
||||
Some(vec!(extension::EightBitMime)));
|
||||
assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-9BITMIME\r\n250 SIZE a")),
|
||||
None);
|
||||
assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-SIZE 42\r\n250 SIZE 43")),
|
||||
Some(vec!(extension::Size(42), extension::Size(43))));
|
||||
assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("")),
|
||||
None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smtp_server_info_supports_feature() {
|
||||
assert_eq!(SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: Some(vec!(extension::EightBitMime))
|
||||
}.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime));
|
||||
assert_eq!(SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime))
|
||||
}.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime));
|
||||
assert_eq!(SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime))
|
||||
}.supports_feature(extension::Size(0)), Ok(extension::Size(42)));
|
||||
assert!(SmtpServerInfo{
|
||||
name: String::from_str("name"),
|
||||
esmtp_features: Some(vec!(extension::EightBitMime))
|
||||
}.supports_feature(extension::Size(42)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
//! Represents a complete SMTP command, ready to be sent to a server
|
||||
#![unstable]
|
||||
|
||||
//! Represents a valid complete SMTP command, ready to be sent to a server
|
||||
|
||||
use std::fmt::{Show, Formatter, Result};
|
||||
/// Supported SMTP commands
|
||||
@@ -80,7 +82,7 @@ impl Show for SmtpCommand {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use smtpcommon::command;
|
||||
use command;
|
||||
|
||||
#[test]
|
||||
fn test_command_fmt() {
|
||||
@@ -103,7 +103,7 @@ mod test {
|
||||
assert_eq!(super::get_first_word("".to_string()), "".to_string());
|
||||
assert_eq!(super::get_first_word("\r\n".to_string()), "".to_string());
|
||||
assert_eq!(super::get_first_word("a\r\n".to_string()), "a".to_string());
|
||||
// Manage cases of empty line, spaces at the beginning, ...
|
||||
// Manage cases of empty line, spaces at the beginning
|
||||
//assert_eq!(super::get_first_word(" a".to_string()), "a".to_string());
|
||||
//assert_eq!(super::get_first_word("\r\n a".to_string()), "a".to_string());
|
||||
assert_eq!(super::get_first_word(" \r\n".to_string()), "".to_string());
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
//! SMTP commands and ESMTP features library
|
||||
|
||||
#![unstable]
|
||||
|
||||
use std::from_str::FromStr;
|
||||
use std::fmt::{Show, Formatter, Result};
|
||||
|
||||
@@ -74,8 +76,8 @@ impl SmtpExtension {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use smtpcommon::extension;
|
||||
use smtpcommon::extension::SmtpExtension;
|
||||
use extension;
|
||||
use extension::SmtpExtension;
|
||||
|
||||
#[test]
|
||||
fn test_extension_same_extension_as() {
|
||||
19
src/lib.rs
19
src/lib.rs
@@ -27,15 +27,15 @@
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```tmp
|
||||
//! ```ignore
|
||||
//! extern crate smtp;
|
||||
//! use std::io::net::tcp::TcpStream;
|
||||
//! use smtp::client::SmtpClient;
|
||||
//! use smtp::smtpc::client::SmtpClient;
|
||||
//! use std::string::String;
|
||||
//!
|
||||
//! let mut email_client: SmtpClient<String, TcpStream> =
|
||||
//! let mut email_client: SmtpClient<TcpStream> =
|
||||
//! SmtpClient::new(String::from_str("localhost"), None, None);
|
||||
//! email_client.send_mail(
|
||||
//! email_client.send_mail::<TcpStream>(
|
||||
//! String::from_str("user@example.com"),
|
||||
//! vec!(String::from_str("user@example.org")),
|
||||
//! String::from_str("Test email")
|
||||
@@ -48,7 +48,8 @@
|
||||
#![desc = "Rust SMTP library"]
|
||||
#![comment = "Simple SMTP client and library"]
|
||||
#![license = "MIT/ASL2"]
|
||||
#![doc(html_root_url = "http://www.rust-ci.org/amousset/rust-smtp/doc")]
|
||||
#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtp/")]
|
||||
#![experimental]
|
||||
|
||||
#![feature(macro_rules)]
|
||||
#![feature(phase)]
|
||||
@@ -61,5 +62,9 @@
|
||||
|
||||
#![feature(phase)] #[phase(plugin, link)] extern crate log;
|
||||
|
||||
pub mod smtpcommon;
|
||||
//pub mod smtpc;
|
||||
pub mod client;
|
||||
pub mod command;
|
||||
pub mod extension;
|
||||
pub mod response;
|
||||
pub mod transaction;
|
||||
pub mod common;
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
//! SMTP responses, contaiing a mandatory return code, and an optional text message
|
||||
|
||||
//extern crate common;
|
||||
#![unstable]
|
||||
|
||||
use std::from_str::FromStr;
|
||||
use std::fmt::{Show, Formatter, Result};
|
||||
use smtpcommon::common::remove_trailing_crlf;
|
||||
use common::remove_trailing_crlf;
|
||||
use std::result;
|
||||
|
||||
/// Contains an SMTP reply, with separed code and message
|
||||
@@ -86,7 +86,7 @@ impl SmtpResponse {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use smtpcommon::response::SmtpResponse;
|
||||
use response::SmtpResponse;
|
||||
|
||||
#[test]
|
||||
fn test_response_fmt() {
|
||||
@@ -1,7 +0,0 @@
|
||||
//! SMTP library
|
||||
|
||||
pub mod common;
|
||||
pub mod command;
|
||||
pub mod extension;
|
||||
pub mod response;
|
||||
pub mod transaction;
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{Show, Formatter};
|
||||
use smtpcommon::command;
|
||||
use smtpcommon::command::SmtpCommand;
|
||||
use command;
|
||||
use command::SmtpCommand;
|
||||
|
||||
/// Contains the state of the current transaction
|
||||
#[deriving(PartialEq,Eq,Clone)]
|
||||
@@ -102,7 +102,7 @@ impl TransactionState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use smtpcommon::command;
|
||||
use command;
|
||||
|
||||
#[test]
|
||||
fn test_transaction_state_is_command_possible() {
|
||||
5
tests/lib.rs
Normal file
5
tests/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[test]
|
||||
|
||||
fn foo() {
|
||||
assert!(true);
|
||||
}
|
||||
Reference in New Issue
Block a user