mirror of https://git.meli.delivery/meli/meli
parent
3f7d962abd
commit
0812242f60
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2017 - 2019 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::email::parser::BytesExt;
|
||||
use crate::error::*;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImapConnection {
|
||||
pub cmd_id: usize,
|
||||
pub stream: native_tls::TlsStream<std::net::TcpStream>,
|
||||
}
|
||||
|
||||
impl Drop for ImapConnection {
|
||||
fn drop(&mut self) {
|
||||
self.send_command(b"LOGOUT").ok().take();
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapConnection {
|
||||
pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
|
||||
let id = format!("M{} ", self.cmd_id - 1);
|
||||
self.read_lines(ret, id)
|
||||
}
|
||||
|
||||
pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> {
|
||||
let mut buf: [u8; 1024] = [0; 1024];
|
||||
ret.clear();
|
||||
let mut last_line_idx: usize = 0;
|
||||
loop {
|
||||
match self.stream.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(b) => {
|
||||
ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) });
|
||||
if let Some(mut pos) = ret[last_line_idx..].rfind("\r\n") {
|
||||
if ret[last_line_idx..].starts_with("* BYE") {
|
||||
return Err(MeliError::new("Disconnected"));
|
||||
}
|
||||
if let Some(prev_line) =
|
||||
ret[last_line_idx..pos + last_line_idx].rfind("\r\n")
|
||||
{
|
||||
last_line_idx += prev_line + 2;
|
||||
pos -= prev_line + 2;
|
||||
}
|
||||
if pos + "\r\n".len() == ret[last_line_idx..].len() {
|
||||
if !termination_string.is_empty()
|
||||
&& ret[last_line_idx..].starts_with(termination_string.as_str())
|
||||
{
|
||||
debug!(&ret[last_line_idx..]);
|
||||
ret.replace_range(last_line_idx.., "");
|
||||
break;
|
||||
} else if termination_string.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
last_line_idx += pos + 2;
|
||||
}
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(MeliError::from(e)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wait_for_continuation_request(&mut self) -> Result<()> {
|
||||
let term = "+ ".to_string();
|
||||
let mut ret = String::new();
|
||||
self.read_lines(&mut ret, term)
|
||||
}
|
||||
|
||||
pub fn send_command(&mut self, command: &[u8]) -> Result<usize> {
|
||||
let command = command.trim();
|
||||
self.stream.write_all(b"M").unwrap();
|
||||
self.stream
|
||||
.write_all(self.cmd_id.to_string().as_bytes())
|
||||
.unwrap();
|
||||
self.stream.write_all(b" ").unwrap();
|
||||
let ret = self.cmd_id;
|
||||
self.cmd_id += 1;
|
||||
self.stream.write_all(command).unwrap();
|
||||
self.stream.write_all(b"\r\n").unwrap();
|
||||
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn send_literal(&mut self, data: &[u8]) -> Result<()> {
|
||||
self.stream.write_all(data).unwrap();
|
||||
if !data.ends_with(b"\r\n") {
|
||||
self.stream.write_all(b"\r\n").unwrap();
|
||||
}
|
||||
self.stream.write_all(b"\r\n").unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_raw(&mut self, raw: &[u8]) -> Result<()> {
|
||||
self.stream.write_all(raw).unwrap();
|
||||
self.stream.write_all(b"\r\n").unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_nonblocking(&mut self, val: bool) -> Result<()> {
|
||||
self.stream.get_mut().set_nonblocking(val)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImapBlockingConnection {
|
||||
buf: [u8; 1024],
|
||||
result: Vec<u8>,
|
||||
prev_res_length: usize,
|
||||
pub conn: ImapConnection,
|
||||
}
|
||||
|
||||
impl From<ImapConnection> for ImapBlockingConnection {
|
||||
fn from(mut conn: ImapConnection) -> Self {
|
||||
conn.stream
|
||||
.get_mut()
|
||||
.set_nonblocking(false)
|
||||
.expect("set_nonblocking call failed");
|
||||
ImapBlockingConnection {
|
||||
buf: [0; 1024],
|
||||
conn,
|
||||
prev_res_length: 0,
|
||||
result: Vec::with_capacity(8 * 1024),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapBlockingConnection {
|
||||
pub fn into_conn(self) -> ImapConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ImapBlockingConnection {
|
||||
type Item = Vec<u8>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.result.drain(0..self.prev_res_length);
|
||||
self.prev_res_length = 0;
|
||||
loop {
|
||||
match self.conn.stream.read(&mut self.buf) {
|
||||
Ok(0) => continue,
|
||||
Ok(b) => {
|
||||
self.result.extend_from_slice(&self.buf[0..b]);
|
||||
debug!(unsafe { std::str::from_utf8_unchecked(&self.result) });
|
||||
if let Some(pos) = self.result.find(b"\r\n") {
|
||||
self.prev_res_length = pos + b"\r\n".len();
|
||||
return Some(self.result[0..self.prev_res_length].to_vec());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2019 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use crate::backends::{BackendFolder, Folder, FolderHash};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ImapFolder {
|
||||
pub(super) hash: FolderHash,
|
||||
pub(super) path: String,
|
||||
pub(super) name: String,
|
||||
pub(super) parent: Option<FolderHash>,
|
||||
pub(super) children: Vec<FolderHash>,
|
||||
}
|
||||
|
||||
impl BackendFolder for ImapFolder {
|
||||
fn hash(&self) -> FolderHash {
|
||||
self.hash
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn change_name(&mut self, s: &str) {
|
||||
self.name = s.to_string();
|
||||
}
|
||||
|
||||
fn children(&self) -> &Vec<FolderHash> {
|
||||
&self.children
|
||||
}
|
||||
|
||||
fn clone(&self) -> Folder {
|
||||
Box::new(ImapFolder {
|
||||
hash: self.hash,
|
||||
path: self.path.clone(),
|
||||
name: self.name.clone(),
|
||||
parent: self.parent,
|
||||
children: self.children.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<FolderHash> {
|
||||
self.parent
|
||||
}
|
||||
}
|
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2017 - 2019 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::backends::BackendOp;
|
||||
use crate::email::*;
|
||||
use crate::error::{MeliError, Result};
|
||||
use std::cell::Cell;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// `BackendOp` implementor for Imap
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImapOp {
|
||||
uid: usize,
|
||||
bytes: Option<String>,
|
||||
headers: Option<String>,
|
||||
body: Option<String>,
|
||||
folder_path: String,
|
||||
flags: Cell<Option<Flag>>,
|
||||
connection: Arc<Mutex<ImapConnection>>,
|
||||
}
|
||||
|
||||
impl ImapOp {
|
||||
pub fn new(uid: usize, folder_path: String, connection: Arc<Mutex<ImapConnection>>) -> Self {
|
||||
ImapOp {
|
||||
uid,
|
||||
connection,
|
||||
bytes: None,
|
||||
headers: None,
|
||||
body: None,
|
||||
folder_path,
|
||||
flags: Cell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendOp for ImapOp {
|
||||
fn description(&self) -> String {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn as_bytes(&mut self) -> Result<&[u8]> {
|
||||
if self.bytes.is_none() {
|
||||
let mut response = String::with_capacity(8 * 1024);
|
||||
{
|
||||
let mut conn = self.connection.lock().unwrap();
|
||||
conn.send_command(format!("SELECT {}", self.folder_path).as_bytes());
|
||||
conn.read_response(&mut response);
|
||||
conn.send_command(format!("UID FETCH {} (FLAGS RFC822)", self.uid).as_bytes())?;
|
||||
conn.read_response(&mut response)?;
|
||||
}
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
response.lines().collect::<Vec<&str>>().len()
|
||||
);
|
||||
match protocol_parser::uid_fetch_response(response.as_bytes())
|
||||
.to_full_result()
|
||||
.map_err(MeliError::from)
|
||||
{
|
||||
Ok(v) => {
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
return Err(MeliError::new(format!(
|
||||
"message with UID {} was not found",
|
||||
self.uid
|
||||
)));
|
||||
}
|
||||
let (uid, flags, b) = v[0];
|
||||
assert_eq!(uid, self.uid);
|
||||
if flags.is_some() {
|
||||
self.flags.set(flags);
|
||||
}
|
||||
self.bytes = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(self.bytes.as_ref().unwrap().as_bytes())
|
||||
}
|
||||
|
||||
fn fetch_headers(&mut self) -> Result<&[u8]> {
|
||||
if self.bytes.is_some() {
|
||||
let result =
|
||||
parser::headers_raw(self.bytes.as_ref().unwrap().as_bytes()).to_full_result()?;
|
||||
return Ok(result);
|
||||
}
|
||||
if self.headers.is_none() {
|
||||
let mut response = String::with_capacity(8 * 1024);
|
||||
let mut conn = self.connection.lock().unwrap();
|
||||
conn.send_command(format!("UID FETCH {} (FLAGS RFC822.HEADER)", self.uid).as_bytes())?;
|
||||
conn.read_response(&mut response)?;
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
response.lines().collect::<Vec<&str>>().len()
|
||||
);
|
||||
match protocol_parser::uid_fetch_response(response.as_bytes())
|
||||
.to_full_result()
|
||||
.map_err(MeliError::from)
|
||||
{
|
||||
Ok(v) => {
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
return Err(MeliError::new(format!(
|
||||
"message with UID {} was not found",
|
||||
self.uid
|
||||
)));
|
||||
}
|
||||
let (uid, flags, b) = v[0];
|
||||
assert_eq!(uid, self.uid);
|
||||
if flags.is_some() {
|
||||
self.flags.set(flags);
|
||||
}
|
||||
self.body = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(self.body.as_ref().unwrap().as_bytes())
|
||||
}
|
||||
|
||||
fn fetch_body(&mut self) -> Result<&[u8]> {
|
||||
if self.bytes.is_some() {
|
||||
let result =
|
||||
parser::body_raw(self.bytes.as_ref().unwrap().as_bytes()).to_full_result()?;
|
||||
return Ok(result);
|
||||
}
|
||||
if self.body.is_none() {
|
||||
let mut response = String::with_capacity(8 * 1024);
|
||||
let mut conn = self.connection.lock().unwrap();
|
||||
conn.send_command(format!("UID FETCH {} (FLAGS RFC822.TEXT)", self.uid).as_bytes())?;
|
||||
conn.read_response(&mut response)?;
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
response.lines().collect::<Vec<&str>>().len()
|
||||
);
|
||||
match protocol_parser::uid_fetch_response(response.as_bytes())
|
||||
.to_full_result()
|
||||
.map_err(MeliError::from)
|
||||
{
|
||||
Ok(v) => {
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
return Err(MeliError::new(format!(
|
||||
"message with UID {} was not found",
|
||||
self.uid
|
||||
)));
|
||||
}
|
||||
let (uid, flags, b) = v[0];
|
||||
assert_eq!(uid, self.uid);
|
||||
if flags.is_some() {
|
||||
self.flags.set(flags);
|
||||
}
|
||||
self.body = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(self.body.as_ref().unwrap().as_bytes())
|
||||
}
|
||||
|
||||
fn fetch_flags(&self) -> Flag {
|
||||
if self.flags.get().is_some() {
|
||||
return self.flags.get().unwrap();
|
||||
}
|
||||
let mut response = String::with_capacity(8 * 1024);
|
||||
let mut conn = self.connection.lock().unwrap();
|
||||
conn.send_command(format!("UID FETCH {} FLAGS", self.uid).as_bytes())
|
||||
.unwrap();
|
||||
conn.read_response(&mut response).unwrap();
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
response.lines().collect::<Vec<&str>>().len()
|
||||
);
|
||||
match protocol_parser::uid_fetch_response(response.as_bytes())
|
||||
.to_full_result()
|
||||
.map_err(MeliError::from)
|
||||
{
|
||||
Ok(v) => {
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
panic!(format!("message with UID {} was not found", self.uid));
|
||||
}
|
||||
let (uid, flags, _) = v[0];
|
||||
assert_eq!(uid, self.uid);
|
||||
if flags.is_some() {
|
||||
self.flags.set(flags);
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e).unwrap(),
|
||||
}
|
||||
self.flags.get().unwrap()
|
||||
}
|
||||
|
||||
fn set_flag(&mut self, _envelope: &mut Envelope, flag: Flag) -> Result<()> {
|
||||
let mut response = String::with_capacity(8 * 1024);
|
||||
let mut conn = self.connection.lock().unwrap();
|
||||
conn.send_command(format!("SELECT \"{}\"", &self.folder_path,).as_bytes())?;
|
||||
conn.read_response(&mut response)?;
|
||||
debug!(&response);
|
||||
conn.send_command(
|
||||
format!(
|
||||
"UID STORE {} FLAGS.SILENT ({})",
|
||||
self.uid,
|
||||
flags_to_imap_list!(flag)
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
conn.read_response(&mut response)?;
|
||||
debug!(&response);
|
||||
match protocol_parser::uid_fetch_response(response.as_bytes())
|
||||
.to_full_result()
|
||||
.map_err(MeliError::from)
|
||||
{
|
||||
Ok(v) => {
|
||||
if v.len() == 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
let (uid, flags, _) = v[0];
|
||||
assert_eq!(uid, self.uid);
|
||||
if flags.is_some() {
|
||||
self.flags.set(flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e).unwrap(),
|
||||
}
|
||||
conn.send_command(format!("EXAMINE \"{}\"", &self.folder_path,).as_bytes())?;
|
||||
conn.read_response(&mut response)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,414 @@
|
||||
use super::*;
|
||||
use nom::{digit, is_digit, rest, IResult};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
|
||||
macro_rules! dbg_dmp (
|
||||
($i: expr, $submac:ident!( $($args:tt)* )) => (
|
||||
{
|
||||
let l = line!();
|
||||
match $submac!($i, $($args)*) {
|
||||
nom::IResult::Error(a) => {
|
||||
debug!("Error({:?}) at l.{} by ' {} '\n{}", a, l, stringify!($submac!($($args)*)), unsafe{ std::str::from_utf8_unchecked($i) });
|
||||
nom::IResult::Error(a)
|
||||
},
|
||||
nom::IResult::Incomplete(a) => {
|
||||
debug!("Incomplete({:?}) at {} by ' {} '\n{}", a, l, stringify!($submac!($($args)*)), unsafe{ std::str::from_utf8_unchecked($i) });
|
||||
nom::IResult::Incomplete(a)
|
||||
},
|
||||
a => a
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
($i:expr, $f:ident) => (
|
||||
dbg_dmp!($i, call!($f));
|
||||
);
|
||||
);
|
||||
macro_rules! get_path_hash {
|
||||
($path:expr) => {{
|
||||
let mut hasher = DefaultHasher::new();
|
||||
$path.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}};
|
||||
}
|
||||
/*
|
||||
* LIST (\HasNoChildren) "." INBOX.Sent
|
||||
* LIST (\HasChildren) "." INBOX
|
||||
*/
|
||||
named!(
|
||||
pub list_folder_result<ImapFolder>,
|
||||
do_parse!(
|
||||
ws!(tag!("* LIST ("))
|
||||
>> properties: take_until!(&b")"[0..])
|
||||
>> tag!(b") ")
|
||||
>> separator: delimited!(tag!(b"\""), take!(1), tag!(b"\""))
|
||||
>> take!(1)
|
||||
>> path: alt_complete!(delimited!(tag!("\""), is_not!("\""), tag!("\"")) | call!(rest))
|
||||
>> ({
|
||||
let separator: u8 = separator[0];
|
||||
let mut f = ImapFolder::default();
|
||||
f.hash = get_path_hash!(path);
|
||||
f.path = String::from_utf8_lossy(path).into();
|
||||
f.name = if let Some(pos) = path.iter().rposition(|&c| c == separator) {
|
||||
f.parent = Some(get_path_hash!(&path[..pos]));
|
||||
String::from_utf8_lossy(&path[pos + 1..]).into()
|
||||
} else {
|
||||
f.path.clone()
|
||||
};
|
||||
|
||||
debug!(f)
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
named!(
|
||||
my_flags<Flag>,
|
||||
do_parse!(
|
||||
flags:
|
||||
separated_nonempty_list!(
|
||||
tag!(" "),
|
||||
preceded!(tag!("\\"), is_not!(")"))
|
||||
)
|
||||
>> ({
|
||||
let mut ret = Flag::default();
|
||||
for f in flags {
|
||||
match f {
|
||||
b"Answered" => {
|
||||
ret.set(Flag::REPLIED, true);
|
||||
}
|
||||
b"Flagged" => {
|
||||
ret.set(Flag::FLAGGED, true);
|
||||
}
|
||||
b"Deleted" => {
|
||||
ret.set(Flag::TRASHED, true);
|
||||
}
|
||||
b"Seen" => {
|
||||
ret.set(Flag::SEEN, true);
|
||||
}
|
||||
b"Draft" => {
|
||||
ret.set(Flag::DRAFT, true);
|
||||
}
|
||||
f => {
|
||||
debug!("unknown Flag token value: {}", unsafe {
|
||||
std::str::from_utf8_unchecked(f)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
*
|
||||
* * 1 FETCH (FLAGS (\Seen) UID 1 RFC822.HEADER {5224}
|
||||
*/
|
||||
named!(
|
||||
pub uid_fetch_response<Vec<(usize, Option<Flag>, &[u8])>>,
|
||||
many0!(
|
||||
do_parse!(
|
||||
tag!("* ")
|
||||
>> take_while!(call!(is_digit))
|
||||
>> tag!(" FETCH (")
|
||||
>> uid_flags: permutation!(preceded!(ws!(tag!("UID ")), map_res!(digit, |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) })), opt!(preceded!(ws!(tag!("FLAGS ")), delimited!(tag!("("), byte_flags, tag!(")")))))
|
||||
>> is_not!("{")
|
||||
>> body: length_bytes!(delimited!(tag!("{"), map_res!(digit, |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) }), tag!("}\r\n")))
|
||||
>> tag!(")\r\n")
|
||||
>> ((uid_flags.0, uid_flags.1, body))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
macro_rules! flags_to_imap_list {
|
||||
($flags:ident) => {{
|
||||
let mut ret = String::new();
|
||||
if !($flags & Flag::REPLIED).is_empty() {
|
||||
ret.push_str("\\Answered");
|
||||
}
|
||||
if !($flags & Flag::FLAGGED).is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push(' ');
|
||||
}
|
||||
ret.push_str("\\Flagged");
|
||||
}
|
||||
if !($flags & Flag::TRASHED).is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push(' ');
|
||||
}
|
||||
ret.push_str("\\Deleted");
|
||||
}
|
||||
if !($flags & Flag::SEEN).is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push(' ');
|
||||
}
|
||||
ret.push_str("\\Seen");
|
||||
}
|
||||
if !($flags & Flag::DRAFT).is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push(' ');
|
||||
}
|
||||
ret.push_str("\\Draft");
|
||||
}
|
||||
ret
|
||||
}};
|
||||
}
|
||||
|
||||
/* Input Example:
|
||||
* ==============
|
||||
*
|
||||
* "M0 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE] Logged in\r\n"
|
||||
*/
|
||||
|
||||
named!(
|
||||
pub capabilities<Vec<&[u8]>>,
|
||||
do_parse!(
|
||||
take_until!("[CAPABILITY ")
|
||||
>> tag!("[CAPABILITY ")
|
||||
>> ret: terminated!(separated_nonempty_list_complete!(tag!(" "), is_not!(" ]")), tag!("]"))
|
||||
>> take_until!("\r\n")
|
||||
>> tag!("\r\n")
|
||||
>> ({ ret })
|
||||
)
|
||||
);
|
||||
|
||||
/// This enum represents the server's untagged responses detailed in `7. Server Responses` of RFC 3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
|
||||
pub enum UntaggedResponse {
|
||||
/// ```text
|
||||
/// 7.4.1. EXPUNGE Response
|
||||
///
|
||||
/// The EXPUNGE response reports that the specified message sequence
|
||||
/// number has been permanently removed from the mailbox. The message
|
||||
/// sequence number for each successive message in the mailbox is
|
||||
/// immediately decremented by 1, and this decrement is reflected in
|
||||
/// message sequence numbers in subsequent responses (including other
|
||||
/// untagged EXPUNGE responses).
|
||||
///
|
||||
/// The EXPUNGE response also decrements the number of messages in the
|
||||
/// mailbox; it is not necessary to send an EXISTS response with the
|
||||
/// new value.
|
||||
///
|
||||
/// As a result of the immediate decrement rule, message sequence
|
||||
/// numbers that appear in a set of successive EXPUNGE responses
|
||||
/// depend upon whether the messages are removed starting from lower
|
||||
/// numbers to higher numbers, or from higher numbers to lower
|
||||
/// numbers. For example, if the last 5 messages in a 9-message
|
||||
/// mailbox are expunged, a "lower to higher" server will send five
|
||||
/// untagged EXPUNGE responses for message sequence number 5, whereas
|
||||
/// a "higher to lower server" will send successive untagged EXPUNGE
|
||||
/// responses for message sequence numbers 9, 8, 7, 6, and 5.
|
||||
///
|
||||
/// An EXPUNGE response MUST NOT be sent when no command is in
|
||||
/// progress, nor while responding to a FETCH, STORE, or SEARCH
|
||||
/// command. This rule is necessary to prevent a loss of
|
||||
/// synchronization of message sequence numbers between client and
|
||||
/// server. A command is not "in progress" until the complete command
|
||||
/// has been received; in particular, a command is not "in progress"
|
||||
/// during the negotiation of command continuation.
|
||||
///
|
||||
/// Note: UID FETCH, UID STORE, and UID SEARCH are different
|
||||
/// commands from FETCH, STORE, and SEARCH. An EXPUNGE
|
||||
/// response MAY be sent during a UID command.
|
||||
///
|
||||
/// The update from the EXPUNGE response MUST be recorded by the
|
||||
/// client.
|
||||
/// ```
|
||||
Expunge(usize),
|
||||
/// ```text
|
||||
/// 7.3.1. EXISTS Response
|
||||
///
|
||||
/// The EXISTS response reports the number of messages in the mailbox.
|
||||
/// This response occurs as a result of a SELECT or EXAMINE command,
|
||||
/// and if the size of the mailbox changes (e.g., new messages).
|
||||
///
|
||||
/// The update from the EXISTS response MUST be recorded by the
|
||||
/// client.
|
||||
/// ```
|
||||
Exists(usize),
|
||||
/// ```text
|
||||
/// 7.3.2. RECENT Response
|
||||
/// The RECENT response reports the number of messages with the
|
||||
/// \Recent flag set. This response occurs as a result of a SELECT or
|
||||
/// EXAMINE command, and if the size of the mailbox changes (e.g., new
|
||||
/// messages).
|
||||
/// ```
|
||||
Recent(usize),
|
||||
}
|
||||
|
||||
named!(
|
||||
pub untagged_responses<Option<UntaggedResponse>>,
|
||||
do_parse!(
|
||||
tag!("* ")
|
||||
>> num: map_res!(digit, |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) })
|
||||
>> tag!(" ")
|
||||
>> tag: take_until!("\r\n")
|
||||
>> tag!("\r\n")
|
||||
>> ({
|
||||
|
||||
use UntaggedResponse::*;
|
||||
match tag {
|
||||
b"EXPUNGE" => Some(Expunge(num)),
|
||||
b"EXISTS" => Some(Exists(num)),
|
||||
b"RECENT" => Some(Recent(num)),
|
||||
_ => {
|
||||
debug!("unknown untagged_response: {}", unsafe { std::str::from_utf8_unchecked(tag) });
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
named!(
|
||||
pub search_results<Vec<usize>>,
|
||||
alt_complete!(do_parse!( tag!("* SEARCH")
|
||||
>> list: separated_nonempty_list_complete!(tag!(" "), map_res!(is_not!("\r\n"), |s| { usize::from_str(unsafe { std::str::from_utf8_unchecked(s) }) }))
|
||||
>> tag!("\r\n")
|
||||
>> ({ list })) |
|
||||
do_parse!(tag!("* SEARCH\r\n") >> ({ Vec::new() })))
|
||||
);
|
||||
|
||||
named!(
|
||||
pub search_results_raw<&[u8]>,
|
||||
alt_complete!(do_parse!( tag!("* SEARCH ")
|
||||
>> list: take_until!("\r\n")
|
||||
>> tag!("\r\n")
|
||||
>> ({ list })) |
|
||||
do_parse!(tag!("* SEARCH\r\n") >> ({ &b""[0..] })))
|
||||
);
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SelectResponse {
|
||||
Ok(SelectResponseOk),
|
||||
Bad(SelectResponseBad),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SelectResponseOk {
|
||||
pub exists: usize,
|
||||
pub recent: usize,
|
||||
pub flags: Flag,
|
||||
pub unseen: usize,
|
||||
pub uidvalidity: usize,
|
||||
pub uidnext: usize,
|
||||
pub permanentflags: Flag,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SelectResponseBad {
|
||||
pub exists: usize,
|
||||
pub recent: usize,
|
||||
pub flags: Flag,
|
||||
}
|
||||
|
||||
/*
|
||||
* Example: C: A142 SELECT INBOX
|
||||
* S: * 172 EXISTS
|
||||
* S: * 1 RECENT
|
||||
* S: * OK [UNSEEN 12] Message 12 is first unseen
|
||||
* S: * OK [UIDVALIDITY 3857529045] UIDs valid
|
||||
* S: * OK [UIDNEXT 4392] Predicted next UID
|
||||
* S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
|
||||
* S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited
|
||||
* S: A142 OK [READ-WRITE] SELECT completed
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
|
||||
* * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted.
|
||||
* * 45 EXISTS
|
||||
* * 0 RECENT
|
||||
* * OK [UNSEEN 16] First unseen.
|
||||
* * OK [UIDVALIDITY 1554422056] UIDs valid
|
||||
* * OK [UIDNEXT 50] Predicted next UID
|
||||
*/
|
||||
pub fn select_response(input: &str) -> IResult<&str, SelectResponse> {
|
||||
if input.contains("* OK") {
|
||||
let mut ret = SelectResponseOk::default();
|
||||
for l in input.split("\r\n") {
|
||||
if l.starts_with("* ") && l.ends_with(" EXISTS") {
|
||||
ret.exists = usize::from_str(&l["* ".len()..l.len()-" EXISTS".len()]).unwrap();
|
||||
} else if l.starts_with("* ") && l.ends_with(" RECENT") {
|
||||
ret.recent = usize::from_str(&l["* ".len()..l.len()-" RECENT".len()]).unwrap();
|
||||
} else if l.starts_with("* FLAGS (") {
|
||||
ret.flags = flags(&l["* FLAGS (".len()..l.len() - ")".len()]).to_full_result().unwrap();
|
||||
} else if l.starts_with("* OK [UNSEEN ") {
|
||||
ret.unseen = usize::from_str(&l["* OK [UNSEEN ".len()..l.find(']').unwrap()]).unwrap();
|
||||
} else if l.starts_with("* OK [UIDVALIDITY ") {
|
||||
ret.uidvalidity = usize::from_str(&l["* OK [UIDVALIDITY ".len()..l.find(']').unwrap()]).unwrap();
|
||||
} else if l.starts_with("* OK [UIDNEXT ") {
|
||||
ret.uidnext = usize::from_str(&l["* OK [UIDNEXT ".len()..l.find(']').unwrap()]).unwrap();
|
||||
} else if l.starts_with("* OK [PERMANENTFLAGS (") {
|
||||
ret.permanentflags = flags(&l["* OK [PERMANENTFLAGS (".len()..l.find(')').unwrap()]).to_full_result().unwrap();
|
||||
} else if !l.is_empty() {
|
||||
debug!("select response: {}", l);
|
||||
}
|
||||
}
|
||||
IResult::Done(&""[0..], SelectResponse::Ok(ret))
|
||||
} else {
|
||||
let mut ret = SelectResponseBad::default();
|
||||
for l in input.split("\r\n") {
|
||||
if l.starts_with("* ") && l.ends_with(" EXISTS") {
|
||||
ret.exists = usize::from_str(&l["* ".len()..l.len()-" EXISTS".len()]).unwrap();
|
||||
} else if l.starts_with("* ") && l.ends_with(" RECENT") {
|
||||
ret.recent = usize::from_str(&l["* ".len()..l.len()-" RECENT".len()]).unwrap();
|
||||
} else if l.starts_with("* FLAGS (") {
|
||||
ret.flags = flags(&l["* FLAGS (".len()..l.len() - ")".len()]).to_full_result().unwrap();
|
||||
} else if !l.is_empty() {
|
||||
debug!("select response: {}", l);
|
||||
}
|
||||
}
|
||||
IResult::Done(&""[0..], SelectResponse::Bad(ret))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flags(input: &str) -> IResult<&str, Flag> {
|
||||
let mut ret = Flag::default();
|
||||
|
||||
let mut input = input;
|
||||
while input.starts_with("\\") {
|
||||
input = &input[1..];
|
||||
let match_end = input.find(|c: char| !c.is_ascii_alphabetic()).or_else(|| input.find(" ")).unwrap_or(input.len());
|
||||
match &input[..match_end] {
|
||||
"Answered" => {
|
||||
ret.set(Flag::REPLIED, true);
|
||||
}
|
||||
"Flagged" => {
|
||||
ret.set(Flag::FLAGGED, true);
|
||||
}
|
||||
"Deleted" => {
|
||||
ret.set(Flag::TRASHED, true);
|
||||
}
|
||||
"Seen" => {
|
||||
ret.set(Flag::SEEN, true);
|
||||
}
|
||||
"Draft" => {
|
||||
ret.set(Flag::DRAFT, true);
|
||||
}
|
||||
f => {
|
||||
debug!("unknown Flag token value: {}", f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
input = &input[match_end..];
|
||||
if input.starts_with(" \\") {
|
||||
input = &input[1..];
|
||||
}
|
||||
}
|
||||
IResult::Done(input, ret)
|
||||
}
|
||||
|
||||
pub fn byte_flags(input: &[u8]) -> IResult<&[u8], Flag> {
|
||||
let i = unsafe{ std::str::from_utf8_unchecked(input) };
|
||||
match flags(i) {
|
||||
IResult::Done(rest, ret) => IResult::Done(rest.as_bytes(), ret),
|
||||
IResult::Error(e) => IResult::Error(e),
|
||||
IResult::Incomplete(e) => IResult::Incomplete(e),
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,571 @@
|
||||
use std::fmt;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
trait Join {
|
||||
fn join(&self, sep: char) -> String;
|
||||
}
|
||||
|
||||
impl<T> Join for [T]
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
fn join(&self, sep: char) -> String {
|
||||
if self.is_empty() {
|
||||
String::from("")
|
||||
} else if self.len() == 1 {
|
||||
format!("{}", self[0])
|
||||
} else {
|
||||
format!("{}{}{}", self[0], sep, self[1..].join(sep))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Search {
|
||||
charset: Option<String>,
|
||||
search_keys: Vec<SearchKey>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Search {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"SEARCH{} {}",
|
||||
if let Some(ch) = self.charset.as_ref() {
|
||||
format!(" CHARSET {}", ch)
|
||||
} else {
|
||||
format!("")
|
||||
},
|
||||
self.search_keys.join(' ')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum SearchKey {
|
||||
All,
|
||||
Answered,
|
||||
Bcc(String),
|
||||
Before(String),
|
||||
Body(String),
|
||||
Cc(String),
|
||||
Deleted,
|
||||
Flagged,
|
||||
From(String),
|
||||
Keyword(FlagKeyword),
|
||||
New,
|
||||
Old,
|
||||
On(String),
|
||||
Recent,
|
||||
Seen,
|
||||
Since(String),
|
||||
Subject(String),
|
||||
Text(String),
|
||||
To(String),
|
||||
Unanswered,
|
||||
Undeleted,
|
||||
Unflagged,
|
||||
Unkeyword(FlagKeyword),
|
||||
Unseen,
|
||||
Draft,
|
||||
Header(String, String), //HeaderFldName
|
||||
Larger(u64),
|
||||
Not(Box<SearchKey>),
|
||||
Or(Box<SearchKey>, Box<SearchKey>),
|
||||
SentBefore(String), //Date
|
||||
SentOn(String), //Date
|
||||
SentSince(String), //Date
|
||||
Smaller(u64),
|
||||
Uid(SequenceSet),
|
||||
Undraft,
|
||||
SequenceSet(SequenceSet),
|
||||
And(Vec<SearchKey>),
|
||||
}
|
||||
impl fmt::Display for SearchKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
SearchKey::All => format!("ALL"),
|
||||
SearchKey::Answered => format!("ANSWERED"),
|
||||
SearchKey::Bcc(ref s) => format!("BCC {}", s),
|
||||
SearchKey::Before(ref s) => format!("BEFORE {}", s),
|
||||
SearchKey::Body(ref s) => format!("BODY {}", s),
|
||||
SearchKey::Cc(ref s) => format!("CC {}", s),
|
||||
SearchKey::Deleted => format!("DELETED"),
|
||||
SearchKey::Flagged => format!("FLAGGED"),
|
||||
SearchKey::From(ref s) => format!("FROM {}", s),
|
||||
SearchKey::Keyword(ref s) => format!("KEYWORD {}", s),
|
||||
SearchKey::New => format!("NEW"),
|
||||
SearchKey::Old => format!("OLD"),
|
||||
SearchKey::On(ref s) => format!("ON {}", s),
|
||||
SearchKey::Recent => format!("RECENT"),
|
||||
SearchKey::Seen => format!("SEEN"),
|
||||
SearchKey::Since(ref s) => format!("SINCE {}", s),
|
||||
SearchKey::Subject(ref s) => format!("SUBJECT {}", s),
|
||||
SearchKey::Text(ref s) => format!("TEXT {}", s),
|
||||
SearchKey::To(ref s) => format!("TO {}", s),
|
||||
SearchKey::Unanswered => format!("UNANSWERED"),
|
||||
SearchKey::Undeleted => format!("UNDELETED"),
|
||||
SearchKey::Unflagged => format!("UNFLAGGED"),
|
||||
SearchKey::Unkeyword(ref s) => format!("UNKEYWORD {}", s),
|
||||
SearchKey::Unseen => format!("UNSEEN"),
|
||||
SearchKey::Draft => format!("DRAFT"),
|
||||
SearchKey::Header(ref name, ref value) => format!("HEADER {} {}", name, value),
|
||||
SearchKey::Larger(ref s) => format!("LARGER {}", s),
|
||||
SearchKey::Not(ref s) => format!("NOT {}", s),
|
||||
SearchKey::Or(ref a, ref b) => format!("OR {} {}", a, b),
|
||||
SearchKey::SentBefore(ref s) => format!("SENTBEFORE {}", s),
|
||||
SearchKey::SentOn(ref s) => format!("SENTON {}", s),
|
||||
SearchKey::SentSince(ref s) => format!("SENTSINCE {}", s),
|
||||
SearchKey::Smaller(ref s) => format!("SMALLER {}", s),
|
||||
SearchKey::Uid(ref s) => format!("UID {}", s),
|
||||
SearchKey::Undraft => format!("UNDRAFT"),
|
||||
SearchKey::SequenceSet(ref s) => format!("SEQUENCESET {}", s),
|
||||
SearchKey::And(ref s) => format!("({})", s.join(' ')),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Delete {
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Delete {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "DELETE {}", self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct Examine {
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Examine {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "EXAMINE {}", self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct Select {
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Select {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "SELECT {}", self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct List {
|
||||
mailbox: Mailbox,
|
||||
list: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for List {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"LIST {} \"{}\"",
|
||||
if self.mailbox.is_empty() {
|
||||
format!("\"\"")
|
||||
} else {
|
||||
format!("{}", self.mailbox)
|
||||
},
|
||||
self.list.as_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Lsub {
|
||||
mailbox: Mailbox,
|
||||
list: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Lsub {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "LSUB {} \"{}\"", self.mailbox, self.list)
|
||||
}
|
||||
}
|
||||
|
||||
enum StatusAttribute {
|
||||
Messages,
|
||||
Recent,
|
||||
UidNext,
|
||||
UidValidity,
|
||||
Unseen,
|
||||
}
|
||||
|
||||
impl fmt::Display for StatusAttribute {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
StatusAttribute::Messages => "MESSAGES",
|
||||
StatusAttribute::Recent => "RECENT",
|
||||
StatusAttribute::UidNext => "UIDNEXT",
|
||||
StatusAttribute::UidValidity => "UIDVALIDITY",
|
||||
StatusAttribute::Unseen => "UNSEEN",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Status {
|
||||
mailbox: Mailbox,
|
||||
status_attributes: Vec<StatusAttribute>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Status {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"STATUS {} ({})",
|
||||
self.mailbox,
|
||||
self.status_attributes.join(' ')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Store {
|
||||
sequence_set: SequenceSet,
|
||||
//store_att_flags:
|
||||
}
|
||||
|
||||
impl fmt::Display for Store {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
unimplemented!()
|
||||
//write!(f, "STORE {}", self.sequence_set)
|
||||
}
|
||||
}
|
||||
|
||||
struct Unsubscribe {
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Unsubscribe {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "UNSUBSCRIBE {}", self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct Subscribe {
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Subscribe {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "SUBSCRIBE {}", self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct Copy {
|
||||
sequence_set: SequenceSet,
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Copy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "COPY {} {}", self.sequence_set, self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct Create {
|
||||
mailbox: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Create {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "CREATE {}", self.mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
struct Rename {
|
||||
from: Mailbox,
|
||||
to: Mailbox,
|
||||
}
|
||||
|
||||
impl fmt::Display for Rename {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "RENAME {} {}", self.from, self.to)
|
||||
}
|
||||
}
|
||||
|
||||
struct Append {
|
||||
mailbox: Mailbox,
|
||||
flag_list: Vec<Flag>,
|
||||
date_time: Option<String>,
|
||||
literal: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Append {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"APPEND {}{}{} {}",
|
||||
self.mailbox,
|
||||
if self.flag_list.is_empty() {
|
||||
String::from("")
|
||||
} else {
|
||||
format!(" {}", self.flag_list.join(' '))
|
||||
},
|
||||
if let Some(date_time) = self.date_time.as_ref() {
|
||||
format!(" {}", date_time)
|
||||
} else {
|
||||
String::from("")
|
||||
},
|
||||
self.literal.as_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Fetch {
|
||||
sequence_set: SequenceSet,
|
||||
}
|
||||
|
||||
impl fmt::Display for Fetch {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "FETCH {}", self.sequence_set)
|
||||
}
|
||||
}
|
||||
|
||||
enum Flag {
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Seen,
|
||||
Draft,
|
||||
/*atom */
|
||||
X(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Flag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"\\{}",
|
||||
match self {
|
||||
Flag::Answered => "Answered",
|
||||
Flag::Flagged => "Flagged",
|
||||
Flag::Deleted => "Deleted",
|
||||
Flag::Seen => "Seen",
|
||||
Flag::Draft => "Draft",
|
||||
Flag::X(ref c) => c.as_str(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum Uid {
|
||||
Copy(Copy),
|
||||
Fetch(Fetch),
|
||||
Search(Search),
|
||||
Store(Store),
|
||||
}
|
||||
|
||||
impl fmt::Display for Uid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"UID {}",
|
||||
match self {
|
||||
Uid::Copy(ref c) => format!("{}", c),
|
||||
Uid::Fetch(ref c) => format!("{}", c),
|
||||
Uid::Search(ref c) => format!("{}", c),
|
||||
Uid::Store(ref c) => format!("{}", c),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum CommandSelect {
|
||||
Check,
|
||||
Close,
|
||||
Expunge,
|
||||
Copy(Copy),
|
||||
Fetch(Fetch),
|
||||
Store(Store),
|
||||
Uid(Uid),
|
||||
Search(Search),
|
||||
}
|
||||
|
||||
impl fmt::Display for CommandSelect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
CommandSelect::Check => format!("CHECK"),
|
||||
CommandSelect::Close => format!("CLOSE"),
|
||||
CommandSelect::Expunge => format!("EXPUNGE"),
|
||||
CommandSelect::Copy(ref c) => format!("{}", c),
|
||||
CommandSelect::Fetch(ref c) => format!("{}", c),
|
||||
CommandSelect::Store(ref c) => format!("{}", c),
|
||||
CommandSelect::Uid(ref c) => format!("{}", c),
|
||||
CommandSelect::Search(ref c) => format!("{}", c),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Valid in all states
|
||||
enum CommandAny {
|
||||
Capability,
|
||||
Logout,
|
||||
Noop,
|
||||
XCommand(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CommandAny {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
CommandAny::Capability => format!("CAPABILITY"),
|
||||
CommandAny::Logout => format!("LOGOUT"),
|
||||
CommandAny::Noop => format!("NOOP"),
|
||||
CommandAny::XCommand(ref x) => format!("{}", x),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum CommandAuth {
|
||||
Append(Append),
|
||||
Create(Create),
|
||||
Delete(Delete),
|
||||
Examine(Examine),
|
||||
List(List),
|
||||
Lsub(Lsub),
|
||||
Rename(Rename),
|
||||
Select(Select),
|
||||
Status(Status),
|
||||
Subscribe(Subscribe),
|
||||
Unsubscribe(Unsubscribe),
|
||||
}
|
||||
|
||||
impl fmt::Display for CommandAuth {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
CommandAuth::Append(ref c) => c.to_string(),
|
||||
CommandAuth::Create(ref c) => c.to_string(),
|
||||
CommandAuth::Delete(ref c) => c.to_string(),
|
||||
CommandAuth::Examine(ref c) => c.to_string(),
|
||||
CommandAuth::List(ref c) => c.to_string(),
|
||||
CommandAuth::Lsub(ref c) => c.to_string(),
|
||||
CommandAuth::Rename(ref c) => c.to_string(),
|
||||
CommandAuth::Select(ref c) => c.to_string(),
|
||||
CommandAuth::Status(ref c) => c.to_string(),
|
||||
CommandAuth::Subscribe(ref c) => c.to_string(),
|
||||
CommandAuth::Unsubscribe(ref c) => c.to_string(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum CommandNonAuth {
|
||||
Login(String, String),
|
||||
Authenticate(String, String),
|
||||
StartTls,
|
||||
}
|
||||
|
||||
impl fmt::Display for CommandNonAuth {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
CommandNonAuth::Login(ref userid, ref password) => {
|
||||
write!(f, "LOGIN \"{}\" \"{}\"", userid, password)
|
||||
}
|
||||
CommandNonAuth::Authenticate(ref auth_type, ref base64) => {
|
||||
write!(f, "AUTHENTICATE \"{}\" \"{}\"", auth_type, base64)
|
||||
}
|
||||
CommandNonAuth::StartTls => write!(f, "STARTTLS"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Command {
|
||||
Any(CommandAny),
|
||||
Auth(CommandAuth),
|
||||
NonAuth(CommandNonAuth),
|
||||
Select(CommandSelect),
|
||||
}
|
||||
impl fmt::Display for Command {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Command::Any(c) => write!(f, "{}", c),
|
||||
Command::Auth(c) => write!(f, "{}", c),
|
||||
Command::NonAuth(c) => write!(f, "{}", c),
|
||||
Command::Select(c) => write!(f, "{}", c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ImapCommand {
|
||||
tag: usize,
|
||||
command: Command,
|
||||
}
|
||||
|
||||
impl fmt::Display for ImapCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} {}\r\n", self.tag, self.command)
|
||||
}
|
||||
}
|
||||
|
||||
enum SeqNumber {
|
||||
MsgNumber(NonZeroUsize),
|
||||
UID(NonZeroUsize),
|
||||
/** "*" represents the largest number in use. In
|
||||
the case of message sequence numbers, it is the number of messages in a
|
||||
non-empty mailbox. In the case of unique identifiers, it is the unique
|
||||
identifier of the last message in the mailbox or, if the mailbox is empty, the
|
||||
mailbox's current UIDNEXT value **/
|
||||
Largest,
|
||||
}
|
||||
|
||||
impl fmt::Display for SeqNumber {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
SeqNumber::MsgNumber(n) => write!(f, "{}", n),
|
||||
SeqNumber::UID(u) => write!(f, "{}", u),
|
||||
SeqNumber::Largest => write!(f, "*"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SeqRange(SeqNumber, SeqNumber);
|
||||
impl fmt::Display for SeqRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}:{}", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
struct SequenceSet {
|
||||
numbers: Vec<SeqNumber>,
|
||||
ranges: Vec<SeqRange>,
|
||||
}
|
||||
impl fmt::Display for SequenceSet {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}",
|
||||
if self.numbers.is_empty() {
|
||||
String::from("")
|
||||
} else {
|
||||
self.numbers.join(',')
|
||||
},
|
||||
if self.ranges.is_empty() {
|
||||
String::from("")
|
||||
} else {
|
||||
self.ranges.join(',')
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type Mailbox = String;
|
||||
type FlagKeyword = String;
|
@ -0,0 +1,35 @@
|
||||
extern crate melib;
|
||||
use melib::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use melib::backends::ImapType;
|
||||
use melib::AccountSettings;
|
||||
use melib::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = std::env::args().skip(1).collect::<Vec<String>>();
|
||||
if args.len() != 3 {
|
||||
eprintln!("Usage: imap_conn server_hostname server_username server_password");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let (a, b, c) = (
|
||||
std::mem::replace(&mut args[0], String::new()),
|
||||
std::mem::replace(&mut args[1], String::new()),
|
||||
std::mem::replace(&mut args[2], String::new()),
|
||||
);
|
||||
let set = AccountSettings {
|
||||
extra: [
|
||||
("server_hostname".to_string(), a),
|
||||
("server_username".to_string(), b),
|
||||
("server_password".to_string(), c),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut imap = ImapType::new(&set);
|
||||
imap.shell();
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue