Various changes, mainly on data types
This commit is contained in:
192
LICENSE-APACHE
192
LICENSE-APACHE
@@ -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,
|
||||
|
||||
44
Makefile
44
Makefile
@@ -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
|
||||
|
||||
10
README.rst
10
README.rst
@@ -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.
|
||||
|
||||
@@ -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