Various changes, mainly on data types

This commit is contained in:
Alexis Mousset
2014-04-28 08:08:17 +02:00
parent 93789d682d
commit 19e85d7cf3
8 changed files with 725 additions and 397 deletions

View File

@@ -1,10 +1,198 @@
Copyright 2014 Alexis Mousset
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

View File

@@ -1,41 +1,47 @@
RUSTC ?= rustc
RUSTDOC ?= rustdoc
RUSTFLAGS ?= -g
VERSION=0.1-pre
BUILDDIR ?= build
INSTALLDIR ?= /usr/local/lib
DOCDIR ?= doc
libsmtp_so=build/libsmtp-4c61a8ad-0.1-pre.so
SMTP_LIB := src/smtp/lib.rs
libsmtp=$(shell $(RUSTC) --crate-file-name $(SMTP_LIB))
smtp_files=\
$(wildcard src/smtp/*.rs) \
$(wildcard src/smtp/client/*.rs)
example_files=\
src/examples/client.rs
$(wildcard src/examples/*.rs)
smtp: $(libsmtp_so)
smtp: $(libsmtp)
$(libsmtp_so): $(smtp_files)
mkdir -p build/
$(RUSTC) $(RUSTFLAGS) src/smtp/lib.rs --out-dir=build
$(libsmtp): $(smtp_files)
mkdir -p $(BUILDDIR)
$(RUSTC) $(RUSTFLAGS) $(SMTP_LIB) --out-dir=$(BUILDDIR)
all: smtp examples docs
all: smtp examples doc
docs: doc/smtp/index.html
doc/smtp/index.html: $(smtp_files)
$(RUSTDOC) src/smtp/lib.rs
doc: $(smtp_files)
$(RUSTDOC) $(SMTP_LIB)
examples: smtp $(example_files)
$(RUSTC) $(RUSTFLAGS) -L build/ src/examples/client.rs -o build/client
$(RUSTC) $(RUSTFLAGS) -L $(BUILDDIR)/ src/examples/client.rs --out-dir=$(BUILDDIR)
build/tests: $(smtp_files)
$(RUSTC) --test -o build/tests src/smtp/lib.rs
$(BUILDDIR)/tests: $(smtp_files)
mkdir -p $(BUILDDIR)/tests
$(RUSTC) --test $(SMTP_LIB) --out-dir=$(BUILDDIR)/tests
check: all build/tests
build/tests --test
check: all $(BUILDDIR)/tests
$(BUILDDIR)/tests/smtp --test
install: $(libsmtp_so)
install $(libsmtp_so) $(INSTALLDIR)
clean:
rm -rf build/
rm -rf doc/
rm -rf $(BUILDDIR)
rm -rf $(DOCDIR)
.PHONY: all smtp examples docs clean check tests

View File

@@ -27,17 +27,15 @@ To run the example:
./build/client
Todo
---
----
- Documentation
- RFC compliance
- Test corevage
- SSL/TLS support
- Client mail and rcpt options
- AUTH support
License
-------
This program is distributed under the Apache license (version 2.0).
This program is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See LICENSE for details.
See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.

View File

@@ -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"));
}

View File

@@ -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)
}
}

View File

@@ -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));
}
}

View File

@@ -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"));
}
}

View File

@@ -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)]