Add structured results for all values using imap-proto (#58)

* First stab at structured types (#28)

Tests currently fail due to djc/imap-proto#2.

LSUB is also broken due to djc/imap-proto#4 (but we don't have tests for
that atm).

* Also parse out RFC822 fetch responses

* Make all the things zero-copy

* Also delegate IntoIterator for ZeroCopy ref

* Fix UNSEEN and LSUB

All tests now pass

* Address @sanmai-NL comments

* Correctly handle incomplete parser responses

* No need for git dep anymore
This commit is contained in:
Jon Gjengset 2018-02-09 11:22:20 -05:00 committed by Matt McCoy
parent dc21eae428
commit 590b80e3a6
12 changed files with 619 additions and 249 deletions

View file

@ -28,6 +28,8 @@ path = "src/lib.rs"
native-tls = "0.1" native-tls = "0.1"
regex = "0.2" regex = "0.2"
bufstream = "0.1" bufstream = "0.1"
imap-proto = "0.3"
nom = "3.2.1"
[dev-dependencies] [dev-dependencies]
base64 = "0.7" base64 = "0.7"

View file

@ -16,7 +16,7 @@ fn main() {
imap_socket.login("username", "password").unwrap(); imap_socket.login("username", "password").unwrap();
match imap_socket.capability() { match imap_socket.capabilities() {
Ok(capabilities) => for capability in capabilities.iter() { Ok(capabilities) => for capability in capabilities.iter() {
println!("{}", capability); println!("{}", capability);
}, },
@ -31,8 +31,8 @@ fn main() {
}; };
match imap_socket.fetch("2", "body[text]") { match imap_socket.fetch("2", "body[text]") {
Ok(lines) => for line in lines.iter() { Ok(msgs) => for msg in &msgs {
print!("{}", line); print!("{:?}", msg);
}, },
Err(e) => println!("Error Fetching email 2: {}", e), Err(e) => println!("Error Fetching email 2: {}", e),
}; };

View file

@ -44,8 +44,8 @@ fn main() {
}; };
match imap_socket.fetch("2", "body[text]") { match imap_socket.fetch("2", "body[text]") {
Ok(lines) => for line in lines.iter() { Ok(msgs) => for msg in &msgs {
print!("{}", line); print!("{:?}", msg);
}, },
Err(e) => println!("Error Fetching email 2: {}", e), Err(e) => println!("Error Fetching email 2: {}", e),
}; };

View file

@ -3,11 +3,12 @@ use native_tls::{TlsConnector, TlsStream};
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::time::Duration; use std::time::Duration;
use bufstream::BufStream; use bufstream::BufStream;
use nom::IResult;
use super::mailbox::Mailbox; use super::types::*;
use super::authenticator::Authenticator; use super::authenticator::Authenticator;
use super::parse::{parse_authenticate_response, parse_capability, parse_response, use super::parse::{parse_authenticate_response, parse_capabilities, parse_fetches, parse_mailbox,
parse_response_ok, parse_select_or_examine}; parse_names};
use super::error::{Error, ParseError, Result, ValidateError}; use super::error::{Error, ParseError, Result, ValidateError};
static TAG_PREFIX: &'static str = "a"; static TAG_PREFIX: &'static str = "a";
@ -88,27 +89,23 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> {
// //
// a) if there's an error, or // a) if there's an error, or
// b) *after* we send DONE // b) *after* we send DONE
let tag = format!("{}{} ", TAG_PREFIX, self.client.tag); let mut v = Vec::new();
let raw_data = try!(self.client.readline()); try!(self.client.readline(&mut v));
let line = String::from_utf8(raw_data).unwrap(); if v.starts_with(b"+") {
if line.starts_with(&tag) { self.done = false;
try!(parse_response(vec![line])); return Ok(());
// We should *only* get a continuation on an error (i.e., it gives BAD or NO).
unreachable!();
} else if !line.starts_with("+") {
return Err(Error::BadResponse(vec![line]));
} }
self.done = false; self.client.read_response_onto(&mut v)?;
Ok(()) // We should *only* get a continuation on an error (i.e., it gives BAD or NO).
unreachable!();
} }
fn terminate(&mut self) -> Result<()> { fn terminate(&mut self) -> Result<()> {
if !self.done { if !self.done {
self.done = true; self.done = true;
try!(self.client.write_line(b"DONE")); try!(self.client.write_line(b"DONE"));
let lines = try!(self.client.read_response()); self.client.read_response().map(|_| ())
parse_response_ok(lines)
} else { } else {
Ok(()) Ok(())
} }
@ -118,7 +115,8 @@ impl<'a, T: Read + Write + 'a> IdleHandle<'a, T> {
/// ///
/// This is necessary so that we can keep using the inner `Client` in `wait_keepalive`. /// This is necessary so that we can keep using the inner `Client` in `wait_keepalive`.
fn wait_inner(&mut self) -> Result<()> { fn wait_inner(&mut self) -> Result<()> {
match self.client.readline().map(|_| ()) { let mut v = Vec::new();
match self.client.readline(&mut v).map(|_| ()) {
Err(Error::Io(ref e)) Err(Error::Io(ref e))
if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock =>
{ {
@ -272,7 +270,8 @@ impl<T: Read + Write> Client<T> {
fn do_auth_handshake<A: Authenticator>(&mut self, authenticator: A) -> Result<()> { fn do_auth_handshake<A: Authenticator>(&mut self, authenticator: A) -> Result<()> {
// TODO Clean up this code // TODO Clean up this code
loop { loop {
let line = try!(self.readline()); let mut line = Vec::new();
try!(self.readline(&mut line));
if line.starts_with(b"+") { if line.starts_with(b"+") {
let data = try!(parse_authenticate_response( let data = try!(parse_authenticate_response(
@ -281,14 +280,8 @@ impl<T: Read + Write> Client<T> {
let auth_response = authenticator.process(data); let auth_response = authenticator.process(data);
try!(self.write_line(auth_response.into_bytes().as_slice())) try!(self.write_line(auth_response.into_bytes().as_slice()))
} else if line.starts_with(format!("{}{} ", TAG_PREFIX, self.tag).as_bytes()) {
try!(parse_response(vec![String::from_utf8(line).unwrap()]));
return Ok(());
} else { } else {
let mut lines = try!(self.read_response()); return self.read_response_onto(&mut line).map(|_| ());
lines.insert(0, String::from_utf8(line).unwrap());
try!(parse_response(lines.clone()));
return Ok(());
} }
} }
} }
@ -304,27 +297,25 @@ impl<T: Read + Write> Client<T> {
/// Selects a mailbox /// Selects a mailbox
pub fn select(&mut self, mailbox_name: &str) -> Result<Mailbox> { pub fn select(&mut self, mailbox_name: &str) -> Result<Mailbox> {
let lines = try!( self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?))
self.run_command_and_read_response(&format!("SELECT {}", validate_str(mailbox_name)?)) .and_then(|lines| parse_mailbox(&lines[..]))
);
parse_select_or_examine(lines)
} }
/// Examine is identical to Select, but the selected mailbox is identified as read-only /// Examine is identical to Select, but the selected mailbox is identified as read-only
pub fn examine(&mut self, mailbox_name: &str) -> Result<Mailbox> { pub fn examine(&mut self, mailbox_name: &str) -> Result<Mailbox> {
let lines = try!( self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?))
self.run_command_and_read_response(&format!("EXAMINE {}", validate_str(mailbox_name)?)) .and_then(|lines| parse_mailbox(&lines[..]))
);
parse_select_or_examine(lines)
} }
/// Fetch retreives data associated with a message in the mailbox. /// Fetch retreives data associated with a message in the mailbox.
pub fn fetch(&mut self, sequence_set: &str, query: &str) -> Result<Vec<String>> { pub fn fetch(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> {
self.run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query)) self.run_command_and_read_response(&format!("FETCH {} {}", sequence_set, query))
.and_then(|lines| parse_fetches(lines))
} }
pub fn uid_fetch(&mut self, uid_set: &str, query: &str) -> Result<Vec<String>> { pub fn uid_fetch(&mut self, uid_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> {
self.run_command_and_read_response(&format!("UID FETCH {} {}", uid_set, query)) self.run_command_and_read_response(&format!("UID FETCH {} {}", uid_set, query))
.and_then(|lines| parse_fetches(lines))
} }
/// Noop always succeeds, and it does nothing. /// Noop always succeeds, and it does nothing.
@ -369,9 +360,9 @@ impl<T: Read + Write> Client<T> {
} }
/// Capability requests a listing of capabilities that the server supports. /// Capability requests a listing of capabilities that the server supports.
pub fn capability(&mut self) -> Result<Vec<String>> { pub fn capabilities(&mut self) -> ZeroCopyResult<Capabilities> {
let lines = try!(self.run_command_and_read_response(&format!("CAPABILITY"))); self.run_command_and_read_response(&format!("CAPABILITY"))
parse_capability(lines) .and_then(|lines| parse_capabilities(lines))
} }
/// Expunge permanently removes all messages that have the \Deleted flag set from the currently /// Expunge permanently removes all messages that have the \Deleted flag set from the currently
@ -392,12 +383,14 @@ impl<T: Read + Write> Client<T> {
} }
/// Store alters data associated with a message in the mailbox. /// Store alters data associated with a message in the mailbox.
pub fn store(&mut self, sequence_set: &str, query: &str) -> Result<Vec<String>> { pub fn store(&mut self, sequence_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> {
self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query)) self.run_command_and_read_response(&format!("STORE {} {}", sequence_set, query))
.and_then(|lines| parse_fetches(lines))
} }
pub fn uid_store(&mut self, uid_set: &str, query: &str) -> Result<Vec<String>> { pub fn uid_store(&mut self, uid_set: &str, query: &str) -> ZeroCopyResult<Vec<Fetch>> {
self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query)) self.run_command_and_read_response(&format!("UID STORE {} {}", uid_set, query))
.and_then(|lines| parse_fetches(lines))
} }
/// Copy copies the specified message to the end of the specified destination mailbox. /// Copy copies the specified message to the end of the specified destination mailbox.
@ -415,12 +408,12 @@ impl<T: Read + Write> Client<T> {
&mut self, &mut self,
reference_name: &str, reference_name: &str,
mailbox_search_pattern: &str, mailbox_search_pattern: &str,
) -> Result<Vec<String>> { ) -> ZeroCopyResult<Vec<Name>> {
self.run_command_and_parse(&format!( self.run_command_and_read_response(&format!(
"LIST {} {}", "LIST {} {}",
quote!(reference_name), quote!(reference_name),
mailbox_search_pattern mailbox_search_pattern
)) )).and_then(|lines| parse_names(lines))
} }
/// The LSUB command returns a subset of names from the set of names /// The LSUB command returns a subset of names from the set of names
@ -429,17 +422,21 @@ impl<T: Read + Write> Client<T> {
&mut self, &mut self,
reference_name: &str, reference_name: &str,
mailbox_search_pattern: &str, mailbox_search_pattern: &str,
) -> Result<Vec<String>> { ) -> ZeroCopyResult<Vec<Name>> {
self.run_command_and_parse(&format!( self.run_command_and_read_response(&format!(
"LSUB {} {}", "LSUB {} {}",
quote!(reference_name), quote!(reference_name),
mailbox_search_pattern mailbox_search_pattern
)) )).and_then(|lines| parse_names(lines))
} }
/// The STATUS command requests the status of the indicated mailbox. /// The STATUS command requests the status of the indicated mailbox.
pub fn status(&mut self, mailbox_name: &str, status_data_items: &str) -> Result<Vec<String>> { pub fn status(&mut self, mailbox_name: &str, status_data_items: &str) -> Result<Mailbox> {
self.run_command_and_parse(&format!("STATUS {} {}", mailbox_name, status_data_items)) self.run_command_and_read_response(&format!(
"STATUS {} {}",
validate_str(mailbox_name)?,
status_data_items
)).and_then(|lines| parse_mailbox(&lines[..]))
} }
/// Returns a handle that can be used to block until the state of the currently selected /// Returns a handle that can be used to block until the state of the currently selected
@ -449,28 +446,24 @@ impl<T: Read + Write> Client<T> {
} }
/// The APPEND command adds a mail to a mailbox. /// The APPEND command adds a mail to a mailbox.
pub fn append(&mut self, folder: &str, content: &[u8]) -> Result<Vec<String>> { pub fn append(&mut self, folder: &str, content: &[u8]) -> Result<()> {
try!(self.run_command(&format!("APPEND \"{}\" {{{}}}", folder, content.len()))); try!(self.run_command(
let line = try!(self.readline()); &format!("APPEND \"{}\" {{{}}}", folder, content.len())
if !line.starts_with(b"+") { ));
let mut v = Vec::new();
try!(self.readline(&mut v));
if !v.starts_with(b"+") {
return Err(Error::Append); return Err(Error::Append);
} }
try!(self.stream.write_all(content)); try!(self.stream.write_all(content));
try!(self.stream.write_all(b"\r\n")); try!(self.stream.write_all(b"\r\n"));
try!(self.stream.flush()); try!(self.stream.flush());
self.read_response() self.read_response().map(|_| ())
} }
/// Runs a command and checks if it returns OK. /// Runs a command and checks if it returns OK.
pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> { pub fn run_command_and_check_ok(&mut self, command: &str) -> Result<()> {
let lines = try!(self.run_command_and_read_response(command)); self.run_command_and_read_response(command).map(|_| ())
parse_response_ok(lines)
}
// Run a command and parse the status response.
pub fn run_command_and_parse(&mut self, command: &str) -> Result<Vec<String>> {
let lines = try!(self.run_command_and_read_response(command));
parse_response(lines)
} }
/// Runs any command passed to it. /// Runs any command passed to it.
@ -479,49 +472,110 @@ impl<T: Read + Write> Client<T> {
self.write_line(command.into_bytes().as_slice()) self.write_line(command.into_bytes().as_slice())
} }
pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result<Vec<String>> { pub fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result<Vec<u8>> {
try!(self.run_command(untagged_command)); try!(self.run_command(untagged_command));
self.read_response() self.read_response()
} }
fn read_response(&mut self) -> Result<Vec<String>> { fn read_response(&mut self) -> Result<Vec<u8>> {
let mut found_tag_line = false; let mut v = Vec::new();
let start_str = format!("{}{} ", TAG_PREFIX, self.tag); self.read_response_onto(&mut v)?;
let mut lines: Vec<String> = Vec::new(); Ok(v)
}
while !found_tag_line { fn read_response_onto(&mut self, data: &mut Vec<u8>) -> Result<()> {
let raw_data = try!(self.readline()); let mut continue_from = None;
let line = String::from_utf8(raw_data) let mut try_first = !data.is_empty();
.map_err(|err| Error::Parse(ParseError::DataNotUtf8(err)))?; let match_tag = format!("{}{}", TAG_PREFIX, self.tag);
lines.push(line.clone()); loop {
if (&*line).starts_with(&*start_str) { let line_start = if try_first {
found_tag_line = true; try_first = false;
0
} else {
let start_new = data.len();
try!(self.readline(data));
continue_from.take().unwrap_or(start_new)
};
let break_with = {
use imap_proto::{parse_response, Response, Status};
let line = &data[line_start..];
match parse_response(line) {
IResult::Done(
_,
Response::Done {
tag,
status,
information,
..
},
) => {
assert_eq!(tag.as_bytes(), match_tag.as_bytes());
Some(match status {
Status::Bad | Status::No => {
Err((status, information.map(|s| s.to_string())))
}
Status::Ok => Ok(()),
status => Err((status, None)),
})
}
IResult::Done(..) => None,
IResult::Incomplete(..) => {
continue_from = Some(line_start);
None
}
_ => Some(Err((Status::Bye, None))),
}
};
match break_with {
Some(Ok(_)) => {
data.truncate(line_start);
break Ok(());
}
Some(Err((status, expl))) => {
use imap_proto::Status;
match status {
Status::Bad => {
break Err(Error::BadResponse(
expl.unwrap_or("no explanation given".to_string()),
))
}
Status::No => {
break Err(Error::NoResponse(
expl.unwrap_or("no explanation given".to_string()),
))
}
_ => break Err(Error::Parse(ParseError::Invalid(data.split_off(0)))),
}
}
None => {}
} }
} }
Ok(lines)
} }
fn read_greeting(&mut self) -> Result<()> { fn read_greeting(&mut self) -> Result<()> {
try!(self.readline()); let mut v = Vec::new();
try!(self.readline(&mut v));
Ok(()) Ok(())
} }
fn readline(&mut self) -> Result<Vec<u8>> { fn readline(&mut self, into: &mut Vec<u8>) -> Result<usize> {
use std::io::BufRead; use std::io::BufRead;
let mut line_buffer: Vec<u8> = Vec::new(); let read = try!(self.stream.read_until(LF, into));
if try!(self.stream.read_until(LF, &mut line_buffer)) == 0 { if read == 0 {
return Err(Error::ConnectionLost); return Err(Error::ConnectionLost);
} }
if self.debug { if self.debug {
// Remove CRLF // Remove CRLF
let len = line_buffer.len(); let len = into.len();
let line = &line_buffer[..(len - 2)]; let line = &into[(len - read - 2)..(len - 2)];
print!("S: {}\n", String::from_utf8_lossy(line)); print!("S: {}\n", String::from_utf8_lossy(line));
} }
Ok(line_buffer) Ok(read)
} }
fn create_command(&mut self, command: String) -> String { fn create_command(&mut self, command: String) -> String {
@ -545,20 +599,27 @@ impl<T: Read + Write> Client<T> {
mod tests { mod tests {
use super::*; use super::*;
use super::super::mock_stream::MockStream; use super::super::mock_stream::MockStream;
use super::super::mailbox::Mailbox;
use super::super::error::Result; use super::super::error::Result;
#[test] #[test]
fn read_response() { fn read_response() {
let response = "a0 OK Logged in.\r\n"; let response = "a0 OK Logged in.\r\n";
let expected_response: Vec<String> = vec![response.to_string()];
let mock_stream = MockStream::new(response.as_bytes().to_vec()); let mock_stream = MockStream::new(response.as_bytes().to_vec());
let mut client = Client::new(mock_stream); let mut client = Client::new(mock_stream);
let actual_response = client.read_response().unwrap(); let actual_response = client.read_response().unwrap();
assert!( assert_eq!(Vec::<u8>::new(), actual_response);
expected_response == actual_response, }
"expected response doesn't equal actual"
);
#[test]
fn fetch_body() {
let response = "a0 OK Logged in.\r\n\
* 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n\
a0 OK FETCH completed\r\n";
let mock_stream = MockStream::new(response.as_bytes().to_vec());
let mut client = Client::new(mock_stream);
client.read_response().unwrap();
client.read_response().unwrap();
} }
@ -578,7 +639,9 @@ mod tests {
.with_buf(greeting.as_bytes().to_vec()) .with_buf(greeting.as_bytes().to_vec())
.with_delay(); .with_delay();
let mut client = Client::new(mock_stream); let mut client = Client::new(mock_stream);
let actual_response = String::from_utf8(client.readline().unwrap()).unwrap(); let mut v = Vec::new();
client.readline(&mut v).unwrap();
let actual_response = String::from_utf8(v).unwrap();
assert_eq!(expected_response, actual_response); assert_eq!(expected_response, actual_response);
} }
@ -586,7 +649,8 @@ mod tests {
fn readline_eof() { fn readline_eof() {
let mock_stream = MockStream::default().with_eof(); let mock_stream = MockStream::default().with_eof();
let mut client = Client::new(mock_stream); let mut client = Client::new(mock_stream);
if let Err(Error::ConnectionLost) = client.readline() { let mut v = Vec::new();
if let Err(Error::ConnectionLost) = client.readline(&mut v) {
} else { } else {
unreachable!("EOF read did not return connection lost"); unreachable!("EOF read did not return connection lost");
} }
@ -598,7 +662,8 @@ mod tests {
// TODO Check the error test // TODO Check the error test
let mock_stream = MockStream::default().with_err(); let mock_stream = MockStream::default().with_err();
let mut client = Client::new(mock_stream); let mut client = Client::new(mock_stream);
client.readline().unwrap(); let mut v = Vec::new();
client.readline(&mut v).unwrap();
} }
#[test] #[test]
@ -735,11 +800,17 @@ mod tests {
a1 OK [READ-ONLY] Select completed.\r\n" a1 OK [READ-ONLY] Select completed.\r\n"
.to_vec(); .to_vec();
let expected_mailbox = Mailbox { let expected_mailbox = Mailbox {
flags: String::from("(\\Answered \\Flagged \\Deleted \\Seen \\Draft)"), flags: vec![
"\\Answered".to_string(),
"\\Flagged".to_string(),
"\\Deleted".to_string(),
"\\Seen".to_string(),
"\\Draft".to_string(),
],
exists: 1, exists: 1,
recent: 1, recent: 1,
unseen: Some(1), unseen: Some(1),
permanent_flags: Some(String::from("()")), permanent_flags: vec![],
uid_next: Some(2), uid_next: Some(2),
uid_validity: Some(1257842737), uid_validity: Some(1257842737),
}; };
@ -752,7 +823,7 @@ mod tests {
client.stream.get_ref().written_buf == command.as_bytes().to_vec(), client.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid examine command" "Invalid examine command"
); );
assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); assert_eq!(mailbox, expected_mailbox);
} }
#[test] #[test]
@ -768,13 +839,24 @@ mod tests {
a1 OK [READ-ONLY] Select completed.\r\n" a1 OK [READ-ONLY] Select completed.\r\n"
.to_vec(); .to_vec();
let expected_mailbox = Mailbox { let expected_mailbox = Mailbox {
flags: String::from("(\\Answered \\Flagged \\Deleted \\Seen \\Draft)"), flags: vec![
"\\Answered".to_string(),
"\\Flagged".to_string(),
"\\Deleted".to_string(),
"\\Seen".to_string(),
"\\Draft".to_string(),
],
exists: 1, exists: 1,
recent: 1, recent: 1,
unseen: Some(1), unseen: Some(1),
permanent_flags: Some(String::from( permanent_flags: vec![
"(\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)", "\\*".to_string(),
)), "\\Answered".to_string(),
"\\Flagged".to_string(),
"\\Deleted".to_string(),
"\\Draft".to_string(),
"\\Seen".to_string(),
],
uid_next: Some(2), uid_next: Some(2),
uid_validity: Some(1257842737), uid_validity: Some(1257842737),
}; };
@ -787,7 +869,7 @@ mod tests {
client.stream.get_ref().written_buf == command.as_bytes().to_vec(), client.stream.get_ref().written_buf == command.as_bytes().to_vec(),
"Invalid select command" "Invalid select command"
); );
assert!(mailbox == expected_mailbox, "Unexpected mailbox returned"); assert_eq!(mailbox, expected_mailbox);
} }
#[test] #[test]
@ -798,15 +880,15 @@ mod tests {
let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"]; let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
let mock_stream = MockStream::new(response); let mock_stream = MockStream::new(response);
let mut client = Client::new(mock_stream); let mut client = Client::new(mock_stream);
let capabilities = client.capability().unwrap(); let capabilities = client.capabilities().unwrap();
assert!( assert!(
client.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(), client.stream.get_ref().written_buf == b"a1 CAPABILITY\r\n".to_vec(),
"Invalid capability command" "Invalid capability command"
); );
assert!( assert_eq!(capabilities.len(), 4);
capabilities == expected_capabilities, for e in expected_capabilities {
"Unexpected capabilities response" assert!(capabilities.has(e));
); }
} }
#[test] #[test]

View file

@ -5,6 +5,7 @@ use std::error::Error as StdError;
use std::net::TcpStream; use std::net::TcpStream;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use imap_proto::Response;
use native_tls::HandshakeError as TlsHandshakeError; use native_tls::HandshakeError as TlsHandshakeError;
use native_tls::Error as TlsError; use native_tls::Error as TlsError;
use bufstream::IntoInnerError as BufError; use bufstream::IntoInnerError as BufError;
@ -21,9 +22,9 @@ pub enum Error {
/// An error from the `native_tls` library while managing the socket. /// An error from the `native_tls` library while managing the socket.
Tls(TlsError), Tls(TlsError),
/// A BAD response from the IMAP server. /// A BAD response from the IMAP server.
BadResponse(Vec<String>), BadResponse(String),
/// A NO response from the IMAP server. /// A NO response from the IMAP server.
NoResponse(Vec<String>), NoResponse(String),
/// The connection was terminated unexpectedly. /// The connection was terminated unexpectedly.
ConnectionLost, ConnectionLost,
// Error parsing a server response. // Error parsing a server response.
@ -58,6 +59,12 @@ impl From<TlsError> for Error {
} }
} }
impl<'a> From<Response<'a>> for Error {
fn from(err: Response<'a>) -> Error {
Error::Parse(ParseError::Unexpected(format!("{:?}", err)))
}
}
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
@ -65,12 +72,9 @@ impl fmt::Display for Error {
Error::Tls(ref e) => fmt::Display::fmt(e, f), Error::Tls(ref e) => fmt::Display::fmt(e, f),
Error::TlsHandshake(ref e) => fmt::Display::fmt(e, f), Error::TlsHandshake(ref e) => fmt::Display::fmt(e, f),
Error::Validate(ref e) => fmt::Display::fmt(e, f), Error::Validate(ref e) => fmt::Display::fmt(e, f),
Error::BadResponse(ref data) => write!( Error::NoResponse(ref data) | Error::BadResponse(ref data) => {
f, write!(f, "{}: {}", &String::from(self.description()), data)
"{}: {}", }
&String::from(self.description()),
&data.join("\n")
),
ref e => f.write_str(e.description()), ref e => f.write_str(e.description()),
} }
} }
@ -105,9 +109,9 @@ impl StdError for Error {
#[derive(Debug)] #[derive(Debug)]
pub enum ParseError { pub enum ParseError {
// Indicates an error parsing the status response. Such as OK, NO, and BAD. // Indicates an error parsing the status response. Such as OK, NO, and BAD.
StatusResponse(Vec<String>), Invalid(Vec<u8>),
// Error parsing the cabability response. // An unexpected response was encountered.
Capability(Vec<String>), Unexpected(String),
// Authentication errors. // Authentication errors.
Authentication(String), Authentication(String),
DataNotUtf8(FromUtf8Error), DataNotUtf8(FromUtf8Error),
@ -124,8 +128,8 @@ impl fmt::Display for ParseError {
impl StdError for ParseError { impl StdError for ParseError {
fn description(&self) -> &str { fn description(&self) -> &str {
match *self { match *self {
ParseError::StatusResponse(_) => "Unable to parse status response", ParseError::Invalid(_) => "Unable to parse status response",
ParseError::Capability(_) => "Unable to parse capability response", ParseError::Unexpected(_) => "Encountered unexpected parsed response",
ParseError::Authentication(_) => "Unable to parse authentication response", ParseError::Authentication(_) => "Unable to parse authentication response",
ParseError::DataNotUtf8(_) => "Unable to parse data as UTF-8 text", ParseError::DataNotUtf8(_) => "Unable to parse data as UTF-8 text",
} }

View file

@ -4,15 +4,19 @@
//! imap is a IMAP client for Rust. //! imap is a IMAP client for Rust.
extern crate bufstream; extern crate bufstream;
extern crate imap_proto;
extern crate native_tls; extern crate native_tls;
extern crate nom;
extern crate regex; extern crate regex;
mod types;
mod parse;
pub mod authenticator; pub mod authenticator;
pub mod client; pub mod client;
pub mod error; pub mod error;
pub mod mailbox;
mod parse; pub use types::*;
#[cfg(test)] #[cfg(test)]
mod mock_stream; mod mock_stream;

View file

@ -1,6 +1,8 @@
use regex::Regex; use regex::Regex;
use nom::IResult;
use imap_proto::{self, Response};
use super::mailbox::Mailbox; use super::types::*;
use super::error::{Error, ParseError, Result}; use super::error::{Error, ParseError, Result};
pub fn parse_authenticate_response(line: String) -> Result<String> { pub fn parse_authenticate_response(line: String) -> Result<String> {
@ -14,102 +16,186 @@ pub fn parse_authenticate_response(line: String) -> Result<String> {
Err(Error::Parse(ParseError::Authentication(line))) Err(Error::Parse(ParseError::Authentication(line)))
} }
pub fn parse_capability(lines: Vec<String>) -> Result<Vec<String>> { enum MapOrNot<T> {
let capability_regex = Regex::new(r"^\* CAPABILITY (.*)\r\n").unwrap(); Map(T),
Not(Response<'static>),
}
//Check Ok unsafe fn parse_many<T, F>(lines: Vec<u8>, mut map: F) -> ZeroCopyResult<Vec<T>>
match parse_response_ok(lines.clone()) { where
Ok(_) => (), F: FnMut(Response<'static>) -> MapOrNot<T>,
Err(e) => return Err(e), {
}; let f = |mut lines| {
let mut things = Vec::new();
loop {
match imap_proto::parse_response(lines) {
IResult::Done(rest, resp) => {
lines = rest;
for line in lines.iter() { match map(resp) {
if capability_regex.is_match(line) { MapOrNot::Map(t) => things.push(t),
let cap = capability_regex.captures(line).unwrap(); MapOrNot::Not(resp) => break Err(resp.into()),
let capabilities_str = cap.get(1).unwrap().as_str(); }
return Ok(capabilities_str.split(' ').map(|x| x.to_string()).collect());
if lines.is_empty() {
break Ok(things);
}
}
_ => {
break Err(Error::Parse(ParseError::Invalid(lines.to_vec())));
}
}
} }
}
Err(Error::Parse(ParseError::Capability(lines)))
}
pub fn parse_response_ok(lines: Vec<String>) -> Result<()> {
match parse_response(lines) {
Ok(_) => Ok(()),
Err(e) => return Err(e),
}
}
pub fn parse_response(lines: Vec<String>) -> Result<Vec<String>> {
let regex = Regex::new(r"^([a-zA-Z0-9]+) (OK|NO|BAD)(.*)").unwrap();
let last_line = match lines.last() {
Some(l) => l,
None => return Err(Error::Parse(ParseError::StatusResponse(lines.clone()))),
}; };
for cap in regex.captures_iter(last_line) { ZeroCopy::new(lines, f)
let response_type = cap.get(2).map(|x| x.as_str()).unwrap_or(""); }
match response_type {
"OK" => return Ok(lines.clone()), pub fn parse_names(lines: Vec<u8>) -> ZeroCopyResult<Vec<Name>> {
"BAD" => return Err(Error::BadResponse(lines.clone())), use imap_proto::MailboxDatum;
"NO" => return Err(Error::NoResponse(lines.clone())), let f = |resp| match resp {
_ => {} // https://github.com/djc/imap-proto/issues/4
Response::MailboxData(MailboxDatum::List {
flags,
delimiter,
name,
}) |
Response::MailboxData(MailboxDatum::SubList {
flags,
delimiter,
name,
}) => MapOrNot::Map(Name {
attributes: flags,
delimiter,
name,
}),
resp => MapOrNot::Not(resp),
};
unsafe { parse_many(lines, f) }
}
pub fn parse_fetches(lines: Vec<u8>) -> ZeroCopyResult<Vec<Fetch>> {
let f = |resp| match resp {
Response::Fetch(num, attrs) => {
let mut fetch = Fetch {
message: num,
flags: vec![],
uid: None,
rfc822: None,
};
for attr in attrs {
use imap_proto::AttributeValue;
match attr {
AttributeValue::Flags(flags) => {
fetch.flags.extend(flags);
}
AttributeValue::Uid(uid) => fetch.uid = Some(uid),
AttributeValue::Rfc822(rfc) => fetch.rfc822 = rfc,
_ => {}
}
}
MapOrNot::Map(fetch)
} }
} resp => MapOrNot::Not(resp),
Err(Error::Parse(ParseError::StatusResponse(lines.clone())))
}
pub fn parse_select_or_examine(lines: Vec<String>) -> Result<Mailbox> {
let exists_regex = Regex::new(r"^\* (\d+) EXISTS\r\n").unwrap();
let recent_regex = Regex::new(r"^\* (\d+) RECENT\r\n").unwrap();
let flags_regex = Regex::new(r"^\* FLAGS (.+)\r\n").unwrap();
let unseen_regex = Regex::new(r"^\* OK \[UNSEEN (\d+)\](.*)\r\n").unwrap();
let uid_validity_regex = Regex::new(r"^\* OK \[UIDVALIDITY (\d+)\](.*)\r\n").unwrap();
let uid_next_regex = Regex::new(r"^\* OK \[UIDNEXT (\d+)\](.*)\r\n").unwrap();
let permanent_flags_regex = Regex::new(r"^\* OK \[PERMANENTFLAGS (.+)\](.*)\r\n").unwrap();
//Check Ok
match parse_response_ok(lines.clone()) {
Ok(_) => (),
Err(e) => return Err(e),
}; };
unsafe { parse_many(lines, f) }
}
pub fn parse_capabilities(lines: Vec<u8>) -> ZeroCopyResult<Capabilities> {
let f = |mut lines| {
use std::collections::HashSet;
let mut caps = HashSet::new();
loop {
match imap_proto::parse_response(lines) {
IResult::Done(rest, Response::Capabilities(c)) => {
lines = rest;
caps.extend(c);
if lines.is_empty() {
break Ok(Capabilities(caps));
}
}
IResult::Done(_, resp) => {
break Err(resp.into());
}
_ => {
break Err(Error::Parse(ParseError::Invalid(lines.to_vec())));
}
}
}
};
unsafe { ZeroCopy::new(lines, f) }
}
pub fn parse_mailbox(mut lines: &[u8]) -> Result<Mailbox> {
let mut mailbox = Mailbox::default(); let mut mailbox = Mailbox::default();
for line in lines.iter() { loop {
if exists_regex.is_match(line) { match imap_proto::parse_response(lines) {
let cap = exists_regex.captures(line).unwrap(); IResult::Done(rest, Response::Data { status, code, .. }) => {
mailbox.exists = cap.get(1).unwrap().as_str().parse::<u32>().unwrap(); lines = rest;
} else if recent_regex.is_match(line) {
let cap = recent_regex.captures(line).unwrap(); if let imap_proto::Status::Ok = status {
mailbox.recent = cap.get(1).unwrap().as_str().parse::<u32>().unwrap(); } else {
} else if flags_regex.is_match(line) { // how can this happen for a Response::Data?
let cap = flags_regex.captures(line).unwrap(); unreachable!();
mailbox.flags = cap.get(1).unwrap().as_str().to_string(); }
} else if unseen_regex.is_match(line) {
let cap = unseen_regex.captures(line).unwrap(); use imap_proto::ResponseCode;
mailbox.unseen = Some(cap.get(1).unwrap().as_str().parse::<u32>().unwrap()); match code {
} else if uid_validity_regex.is_match(line) { Some(ResponseCode::UidValidity(uid)) => {
let cap = uid_validity_regex.captures(line).unwrap(); mailbox.uid_validity = Some(uid);
mailbox.uid_validity = Some(cap.get(1).unwrap().as_str().parse::<u32>().unwrap()); }
} else if uid_next_regex.is_match(line) { Some(ResponseCode::UidNext(unext)) => {
let cap = uid_next_regex.captures(line).unwrap(); mailbox.uid_next = Some(unext);
mailbox.uid_next = Some(cap.get(1).unwrap().as_str().parse::<u32>().unwrap()); }
} else if permanent_flags_regex.is_match(line) { Some(ResponseCode::Unseen(n)) => {
let cap = permanent_flags_regex.captures(line).unwrap(); mailbox.unseen = Some(n);
mailbox.permanent_flags = Some(cap.get(1).unwrap().as_str().to_string()); }
Some(ResponseCode::PermanentFlags(flags)) => {
mailbox
.permanent_flags
.extend(flags.into_iter().map(|s| s.to_string()));
}
_ => {}
}
}
IResult::Done(rest, Response::MailboxData(m)) => {
lines = rest;
use imap_proto::MailboxDatum;
match m {
MailboxDatum::Exists(e) => {
mailbox.exists = e;
}
MailboxDatum::Recent(r) => {
mailbox.recent = r;
}
MailboxDatum::Flags(flags) => {
mailbox
.flags
.extend(flags.into_iter().map(|s| s.to_string()));
}
MailboxDatum::SubList { .. } | MailboxDatum::List { .. } => {}
}
}
IResult::Done(_, resp) => {
break Err(resp.into());
}
_ => {
break Err(Error::Parse(ParseError::Invalid(lines.to_vec())));
}
}
if lines.is_empty() {
break Ok(mailbox);
} }
} }
Ok(mailbox)
} }
#[cfg(test)] #[cfg(test)]
@ -118,51 +204,46 @@ mod tests {
#[test] #[test]
fn parse_capability_test() { fn parse_capability_test() {
let expected_capabilities = vec![ let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
String::from("IMAP4rev1"), let lines = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n";
String::from("STARTTLS"), let capabilities = parse_capabilities(lines.to_vec()).unwrap();
String::from("AUTH=GSSAPI"), assert_eq!(capabilities.len(), 4);
String::from("LOGINDISABLED"), for e in expected_capabilities {
]; assert!(capabilities.has(e));
let lines = vec![ }
String::from("* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"),
String::from("a1 OK CAPABILITY completed\r\n"),
];
let capabilities = parse_capability(lines).unwrap();
assert!(
capabilities == expected_capabilities,
"Unexpected capabilities parse response"
);
} }
#[test] #[test]
#[should_panic] #[should_panic]
fn parse_capability_invalid_test() { fn parse_capability_invalid_test() {
let lines = vec![ let lines = b"* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n";
String::from("* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"), parse_capabilities(lines.to_vec()).unwrap();
String::from("a1 OK CAPABILITY completed\r\n"),
];
parse_capability(lines).unwrap();
} }
#[test] #[test]
fn parse_response_test() { fn parse_names_test() {
let lines = vec![ let lines = b"* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n";
String::from("* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"), let names = parse_names(lines.to_vec()).unwrap();
String::from("a2 OK List completed.\r\n"), assert_eq!(names.len(), 1);
]; assert_eq!(names[0].attributes(), &["\\HasNoChildren"]);
let expected_lines = lines.clone(); assert_eq!(names[0].delimiter(), ".");
let actual_lines = parse_response(lines).unwrap(); assert_eq!(names[0].name(), "INBOX");
assert!(expected_lines == actual_lines, "Unexpected parse response");
} }
#[test] #[test]
#[should_panic] fn parse_fetches_test() {
fn parse_response_invalid_test() { let lines = b"\
let lines = vec![ * 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n\
String::from("* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"), * 25 FETCH (FLAGS (\\Seen))\r\n";
String::from("a2 BAD broken.\r\n"), let fetches = parse_fetches(lines.to_vec()).unwrap();
]; assert_eq!(fetches.len(), 2);
parse_response(lines).unwrap(); assert_eq!(fetches[0].message, 24);
assert_eq!(fetches[0].flags(), &["\\Seen"]);
assert_eq!(fetches[0].uid, Some(4827943));
assert_eq!(fetches[0].rfc822(), None);
assert_eq!(fetches[1].message, 25);
assert_eq!(fetches[1].flags(), &["\\Seen"]);
assert_eq!(fetches[1].uid, None);
assert_eq!(fetches[1].rfc822(), None);
} }
} }

29
src/types/capabilities.rs Normal file
View file

@ -0,0 +1,29 @@
// Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
use std::collections::HashSet;
use std::collections::hash_set::Iter;
pub struct Capabilities(pub(crate) HashSet<&'static str>);
use std::hash::Hash;
use std::borrow::Borrow;
impl Capabilities {
pub fn has<S: ?Sized>(&self, s: &S) -> bool
where
for<'a> &'a str: Borrow<S>,
S: Hash + Eq,
{
self.0.contains(s)
}
pub fn iter<'a>(&'a self) -> Iter<'a, &'a str> {
self.0.iter()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

19
src/types/fetch.rs Normal file
View file

@ -0,0 +1,19 @@
// Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
#[derive(Debug, Eq, PartialEq)]
pub struct Fetch {
pub message: u32,
pub(crate) flags: Vec<&'static str>,
pub uid: Option<u32>,
pub(crate) rfc822: Option<&'static [u8]>,
}
impl Fetch {
pub fn flags<'a>(&'a self) -> &'a [&'a str] {
&self.flags[..]
}
pub fn rfc822<'a>(&'a self) -> Option<&'a [u8]> {
self.rfc822
}
}

View file

@ -2,11 +2,11 @@ use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Mailbox { pub struct Mailbox {
pub flags: String, pub flags: Vec<String>,
pub exists: u32, pub exists: u32,
pub recent: u32, pub recent: u32,
pub unseen: Option<u32>, pub unseen: Option<u32>,
pub permanent_flags: Option<String>, pub permanent_flags: Vec<String>,
pub uid_next: Option<u32>, pub uid_next: Option<u32>,
pub uid_validity: Option<u32>, pub uid_validity: Option<u32>,
} }
@ -14,11 +14,11 @@ pub struct Mailbox {
impl Default for Mailbox { impl Default for Mailbox {
fn default() -> Mailbox { fn default() -> Mailbox {
Mailbox { Mailbox {
flags: "".to_string(), flags: Vec::new(),
exists: 0, exists: 0,
recent: 0, recent: 0,
unseen: None, unseen: None,
permanent_flags: None, permanent_flags: Vec::new(),
uid_next: None, uid_next: None,
uid_validity: None, uid_validity: None,
} }
@ -29,7 +29,7 @@ impl fmt::Display for Mailbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( write!(
f, f,
"flags: {}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\ "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\
uid_next: {:?}, uid_validity: {:?}", uid_next: {:?}, uid_validity: {:?}",
self.flags, self.flags,
self.exists, self.exists,

127
src/types/mod.rs Normal file
View file

@ -0,0 +1,127 @@
mod mailbox;
pub use self::mailbox::Mailbox;
mod fetch;
pub use self::fetch::Fetch;
mod name;
pub use self::name::Name;
mod capabilities;
pub use self::capabilities::Capabilities;
pub struct ZeroCopy<D> {
owned: Box<[u8]>,
derived: D,
}
impl<D> ZeroCopy<D> {
/// Derive a new `ZeroCopy` view of the byte data stored in `owned`.
///
/// # Safety
///
/// The `derive` callback will be passed a `&'static [u8]`. However, this reference is not, in
/// fact `'static`. Instead, it is only valid for as long as the `ZeroCopy` lives. Therefore,
/// it is *only* safe to call this function if *every* accessor on `D` returns either a type
/// that does not contain any borrows, *or* where the return type is bound to the lifetime of
/// `&self`.
///
/// It is *not* safe for the error type `E` to borrow from the passed reference.
pub unsafe fn new<F, E>(owned: Vec<u8>, derive: F) -> Result<Self, E>
where
F: FnOnce(&'static [u8]) -> Result<D, E>,
{
use std::mem;
// the memory pointed to by `owned` now has a stable address (on the heap).
// even if we move the `Box` (i.e., into `ZeroCopy`), a slice to it will remain valid.
let owned = owned.into_boxed_slice();
// this is the unsafe part -- the implementor of `derive` must be aware that the reference
// they are passed is not *really* 'static, but rather the lifetime of `&self`.
let static_owned_ref: &'static [u8] = mem::transmute(&*owned);
Ok(ZeroCopy {
owned,
derived: derive(static_owned_ref)?,
})
}
}
use super::error::Error;
pub type ZeroCopyResult<T> = Result<ZeroCopy<T>, Error>;
use std::ops::Deref;
impl<D> Deref for ZeroCopy<D> {
type Target = D;
fn deref(&self) -> &Self::Target {
&self.derived
}
}
// re-implement standard traits
// basically copied from Rc
impl<D: PartialEq> PartialEq for ZeroCopy<D> {
fn eq(&self, other: &ZeroCopy<D>) -> bool {
**self == **other
}
fn ne(&self, other: &ZeroCopy<D>) -> bool {
**self != **other
}
}
impl<D: Eq> Eq for ZeroCopy<D> {}
use std::cmp::Ordering;
impl<D: PartialOrd> PartialOrd for ZeroCopy<D> {
fn partial_cmp(&self, other: &ZeroCopy<D>) -> Option<Ordering> {
(**self).partial_cmp(&**other)
}
fn lt(&self, other: &ZeroCopy<D>) -> bool {
**self < **other
}
fn le(&self, other: &ZeroCopy<D>) -> bool {
**self <= **other
}
fn gt(&self, other: &ZeroCopy<D>) -> bool {
**self > **other
}
fn ge(&self, other: &ZeroCopy<D>) -> bool {
**self >= **other
}
}
impl<D: Ord> Ord for ZeroCopy<D> {
fn cmp(&self, other: &ZeroCopy<D>) -> Ordering {
(**self).cmp(&**other)
}
}
use std::hash::{Hash, Hasher};
impl<D: Hash> Hash for ZeroCopy<D> {
fn hash<H: Hasher>(&self, state: &mut H) {
(**self).hash(state);
}
}
use std::fmt;
impl<D: fmt::Display> fmt::Display for ZeroCopy<D> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&**self, f)
}
}
impl<D: fmt::Debug> fmt::Debug for ZeroCopy<D> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&**self, f)
}
}
impl<'a, D> IntoIterator for &'a ZeroCopy<D>
where
&'a D: IntoIterator,
{
type Item = <&'a D as IntoIterator>::Item;
type IntoIter = <&'a D as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
(**self).into_iter()
}
}

22
src/types/name.rs Normal file
View file

@ -0,0 +1,22 @@
// Note that none of these fields are *actually* 'static.
// Rather, they are tied to the lifetime of the `ZeroCopy` that contains this `Name`.
#[derive(Debug, Eq, PartialEq)]
pub struct Name {
pub(crate) attributes: Vec<&'static str>,
pub(crate) delimiter: &'static str,
pub(crate) name: &'static str,
}
impl Name {
pub fn attributes<'a>(&'a self) -> &'a [&'a str] {
&self.attributes[..]
}
pub fn delimiter<'a>(&'a self) -> &'a str {
self.delimiter
}
pub fn name<'a>(&'a self) -> &'a str {
self.name
}
}