Add tests for Extension and Response

This commit is contained in:
Alexis Mousset
2015-03-14 15:38:23 +01:00
parent 1469fc8660
commit 658e7937d7
11 changed files with 205 additions and 123 deletions

View File

@@ -1,7 +1,7 @@
[package]
name = "smtp"
version = "0.0.7"
version = "0.0.8"
description = "Simple SMTP client"
readme = "README.md"
documentation = "http://amousset.github.io/rust-smtp/smtp/"

View File

@@ -15,7 +15,7 @@ use crypto::hmac::Hmac;
use crypto::md5::Md5;
use crypto::mac::Mac;
use tools::NUL;
use NUL;
/// Returns a PLAIN mecanism response
pub fn plain(username: &str, password: &str) -> String {

View File

@@ -15,16 +15,34 @@ use std::net::TcpStream;
use std::net::{SocketAddr, ToSocketAddrs};
use std::io::{BufRead, BufStream, Read, Write};
use tools::{CRLF, MESSAGE_ENDING};
use tools::{escape_dot, escape_crlf};
use response::{Response, Severity, Category};
use error::SmtpResult;
use client::connecter::Connecter;
use client::authentication::{plain, cram_md5};
use {CRLF, MESSAGE_ENDING};
pub mod connecter;
mod authentication;
/// Returns the string after adding a dot at the beginning of each line starting with a dot
///
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
#[inline]
fn escape_dot(string: &str) -> String {
if string.starts_with(".") {
format!(".{}", string)
} else {
string.to_string()
}.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
#[inline]
fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CR><LF>")
}
/// Structure that implements the SMTP client
pub struct Client<S = TcpStream> {
/// TCP stream between client and server
@@ -210,3 +228,26 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
}
}
}
#[cfg(test)]
mod test {
use super::{escape_dot, escape_crlf};
#[test]
fn test_escape_dot() {
assert_eq!(escape_dot(".test").as_slice(), "..test");
assert_eq!(escape_dot("\r.\n.\r\n").as_slice(), "\r..\n..\r\n");
assert_eq!(escape_dot("test\r\n.test\r\n").as_slice(), "test\r\n..test\r\n");
assert_eq!(escape_dot("test\r\n.\r\ntest").as_slice(), "test\r\n..\r\ntest");
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n").as_slice(), "<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\n").as_slice(), "EHLO my_name<CR><LF>");
assert_eq!(
escape_crlf("EHLO my_name\r\nSIZE 42\r\n").as_slice(),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
);
}
}

View File

@@ -83,3 +83,8 @@ impl FromError<&'static str> for SmtpError {
/// SMTP result type
pub type SmtpResult = Result<Response, SmtpError>;
#[cfg(test)]
mod test {
// TODO
}

View File

@@ -79,6 +79,7 @@ impl Extension {
#[cfg(test)]
mod test {
use super::Extension;
use response::{Severity, Category, Response};
#[test]
fn test_from_str() {
@@ -89,17 +90,19 @@ mod test {
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
}
// #[test]
// fn test_parse_esmtp_response() {
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 SIZE 42"),
// vec![Extension::EightBitMime]);
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 AUTH PLAIN CRAM-MD5\r\n250 UNKNON 42"),
// vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-9BITMIME\r\n250 SIZE a"),
// vec![]);
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-SIZE 42\r\n250 SIZE 43"),
// vec![]);
// assert_eq!(Extension::parse_esmtp_response(""),
// vec![]);
// }
#[test]
fn test_parse_esmtp_response() {
assert_eq!(Extension::parse_esmtp_response(&Response::new(
"2".parse::<Severity>().unwrap(),
"2".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
)), vec![Extension::EightBitMime]);
assert_eq!(Extension::parse_esmtp_response(&Response::new(
"4".parse::<Severity>().unwrap(),
"3".parse::<Category>().unwrap(),
3,
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()]
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
}
}

View File

@@ -149,7 +149,6 @@ extern crate crypto;
extern crate time;
extern crate uuid;
mod tools;
mod extension;
pub mod client;
pub mod sender;
@@ -169,3 +168,20 @@ pub static SMTPS_PORT: u16 = 465;
/// Default submission port
pub static SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";

View File

@@ -11,7 +11,7 @@
use std::fmt::{Display, Formatter, Result};
use tools::SP;
use SP;
/// Converts an adress or an address with an alias to an `Address`
pub trait ToAddress {

View File

@@ -13,8 +13,8 @@ use time::Tm;
use std::fmt::{Display, Formatter, Result};
use tools::{SP, COLON};
use mailer::address::Address;
use {COLON, SP};
/// Converts to an `Header`
pub trait ToHeader {

View File

@@ -15,8 +15,8 @@ use time::{now, Tm};
use mailer::header::{ToHeader, Header};
use mailer::address::ToAddress;
use tools::CRLF;
use sendable_email::SendableEmail;
use CRLF;
pub mod header;
pub mod address;

View File

@@ -222,4 +222,123 @@ mod test {
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4).as_slice(), "4");
}
#[test]
fn test_response_new() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
), Response {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
});
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
assert!(! Response::new(
"4".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PositiveCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).category(), Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).detail(), 1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).code(), "241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(241));
assert!(! Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec![]
).first_word(), None);
}
}

View File

@@ -1,102 +0,0 @@
// 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.
//! Tools for common string manipulations
use std::string::String;
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Carriage return
pub static CR: &'static str = "\r";
/// Line feed
pub static LF: &'static str = "\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";
/// Returns the first word of a string, or the string if it contains no space
#[inline]
pub fn get_first_word(string: &str) -> &str {
match string.lines_any().next() {
Some(line) => match line.words().next() {
Some(word) => word,
None => "",
},
None => "",
}
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
#[inline]
pub fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CR><LF>")
}
/// Returns the string after adding a dot at the beginning of each line starting with a dot
///
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
#[inline]
pub fn escape_dot(string: &str) -> String {
if string.starts_with(".") {
format!(".{}", string)
} else {
string.to_string()
}.replace(format!("{}.", CR).as_slice(), format!("{}..", CR).as_slice())
.replace(format!("{}.", LF).as_slice(), format!("{}..", LF).as_slice())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_first_word() {
assert_eq!(get_first_word("first word"), "first");
assert_eq!(get_first_word("first word\r\ntest"), "first");
assert_eq!(get_first_word("first"), "first");
assert_eq!(get_first_word(""), "");
assert_eq!(get_first_word("\r\n"), "");
assert_eq!(get_first_word("a\r\n"), "a");
// Manage cases of empty line, spaces at the beginning
//assert_eq!(get_first_word(" a"), "a");
//assert_eq!(get_first_word("\r\n a"), "a");
assert_eq!(get_first_word(" \r\n"), "");
assert_eq!(get_first_word("\r\n "), "");
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n").as_slice(), "<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\n").as_slice(), "EHLO my_name<CR><LF>");
assert_eq!(
escape_crlf("EHLO my_name\r\nSIZE 42\r\n").as_slice(),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
);
}
#[test]
fn test_escape_dot() {
assert_eq!(escape_dot(".test").as_slice(), "..test");
assert_eq!(escape_dot("\r.\n.\r\n").as_slice(), "\r..\n..\r\n");
assert_eq!(escape_dot("test\r\n.test\r\n").as_slice(), "test\r\n..test\r\n");
assert_eq!(escape_dot("test\r\n.\r\ntest").as_slice(), "test\r\n..\r\ntest");
}
}