parent
3f7d962abd
commit
0812242f60
|
@ -19,6 +19,7 @@ notify = "4.0.1"
|
||||||
notify-rust = "^3"
|
notify-rust = "^3"
|
||||||
termion = "1.5.1"
|
termion = "1.5.1"
|
||||||
xdg = "2.1.0"
|
xdg = "2.1.0"
|
||||||
|
native-tls = "0.2"
|
||||||
serde = "1.0.71"
|
serde = "1.0.71"
|
||||||
serde_derive = "1.0.71"
|
serde_derive = "1.0.71"
|
||||||
bincode = "1.0.1"
|
bincode = "1.0.1"
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub mod imap;
|
||||||
pub mod maildir;
|
pub mod maildir;
|
||||||
pub mod mbox;
|
pub mod mbox;
|
||||||
|
|
||||||
|
pub use self::imap::ImapType;
|
||||||
use crate::async_workers::*;
|
use crate::async_workers::*;
|
||||||
use crate::conf::AccountSettings;
|
use crate::conf::AccountSettings;
|
||||||
use crate::error::{MeliError, Result};
|
use crate::error::{MeliError, Result};
|
||||||
|
@ -63,7 +64,10 @@ impl Backends {
|
||||||
"mbox".to_string(),
|
"mbox".to_string(),
|
||||||
Box::new(|| Box::new(|f| Box::new(MboxType::new(f)))),
|
Box::new(|| Box::new(|f| Box::new(MboxType::new(f)))),
|
||||||
);
|
);
|
||||||
//b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new(""))));
|
b.register(
|
||||||
|
"imap".to_string(),
|
||||||
|
Box::new(|| Box::new(|f| Box::new(ImapType::new(f)))),
|
||||||
|
);
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,11 +165,10 @@ pub trait MailBackend: ::std::fmt::Debug {
|
||||||
fn folders(&self) -> FnvHashMap<FolderHash, Folder>;
|
fn folders(&self) -> FnvHashMap<FolderHash, Folder>;
|
||||||
fn operation(&self, hash: EnvelopeHash, folder_hash: FolderHash) -> Box<BackendOp>;
|
fn operation(&self, hash: EnvelopeHash, folder_hash: FolderHash) -> Box<BackendOp>;
|
||||||
|
|
||||||
fn save(&self, bytes: &[u8], folder: &str) -> Result<()>;
|
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()>;
|
||||||
fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> {
|
fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
//login function
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `BackendOp` manages common operations for the various mail backends. They only live for the
|
/// A `BackendOp` manages common operations for the various mail backends. They only live for the
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* meli - mailbox module.
|
* meli - imap module.
|
||||||
*
|
*
|
||||||
* Copyright 2017 Manos Pitsidianakis
|
* Copyright 2019 Manos Pitsidianakis
|
||||||
*
|
*
|
||||||
* This file is part of meli.
|
* This file is part of meli.
|
||||||
*
|
*
|
||||||
|
@ -19,56 +19,816 @@
|
||||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
#[macro_use]
|
||||||
use async::*;
|
mod protocol_parser;
|
||||||
use error::Result;
|
pub use protocol_parser::{UntaggedResponse::*, *};
|
||||||
use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder};
|
mod folder;
|
||||||
use mailbox::email::Envelope;
|
pub use folder::*;
|
||||||
|
mod operations;
|
||||||
|
pub use operations::*;
|
||||||
|
mod connection;
|
||||||
|
pub use connection::*;
|
||||||
|
|
||||||
/// `BackendOp` implementor for Imap
|
extern crate native_tls;
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct ImapOp {}
|
|
||||||
|
|
||||||
impl ImapOp {
|
use crate::async_workers::{Async, AsyncBuilder, AsyncStatus};
|
||||||
pub fn new(_path: String) -> Self {
|
use crate::backends::BackendOp;
|
||||||
ImapOp {}
|
use crate::backends::FolderHash;
|
||||||
}
|
use crate::backends::RefreshEvent;
|
||||||
}
|
use crate::backends::RefreshEventKind::{self, *};
|
||||||
|
use crate::backends::{BackendFolder, Folder, MailBackend, RefreshEventConsumer};
|
||||||
|
use crate::conf::AccountSettings;
|
||||||
|
use crate::email::*;
|
||||||
|
use crate::error::{MeliError, Result};
|
||||||
|
use fnv::{FnvHashMap, FnvHashSet};
|
||||||
|
use native_tls::TlsConnector;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
pub type UID = usize;
|
||||||
|
|
||||||
|
|
||||||
impl BackendOp for ImapOp {
|
|
||||||
fn description(&self) -> String {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
fn as_bytes(&mut self) -> Result<&[u8]> {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
fn fetch_headers(&mut self) -> Result<&[u8]> {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
fn fetch_body(&mut self) -> Result<&[u8]> {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
fn fetch_flags(&self) -> Flag {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Imap backend
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImapType {}
|
pub struct ImapType {
|
||||||
|
account_name: String,
|
||||||
|
server_hostname: String,
|
||||||
|
server_username: String,
|
||||||
|
server_password: String,
|
||||||
|
connection: Arc<Mutex<ImapConnection>>,
|
||||||
|
capabilities: FnvHashSet<Vec<u8>>,
|
||||||
|
folders: FnvHashMap<FolderHash, ImapFolder>,
|
||||||
|
folder_connections: FnvHashMap<FolderHash, Arc<Mutex<ImapConnection>>>,
|
||||||
|
hash_index: Arc<Mutex<FnvHashMap<EnvelopeHash, (UID, FolderHash)>>>,
|
||||||
|
uid_index: Arc<Mutex<FnvHashMap<usize, EnvelopeHash>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl MailBackend for ImapType {
|
impl MailBackend for ImapType {
|
||||||
fn get(&self, _folder: &Folder) -> Async<Result<Vec<Envelope>>> {
|
fn get(&mut self, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
|
||||||
unimplemented!();
|
macro_rules! exit_on_error {
|
||||||
|
($tx:expr,$($result:expr)+) => {
|
||||||
|
$(if let Err(e) = $result {
|
||||||
|
$tx.send(AsyncStatus::Payload(Err(e)));
|
||||||
|
std::process::exit(1);
|
||||||
|
})+
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut w = AsyncBuilder::new();
|
||||||
|
let handle = {
|
||||||
|
let tx = w.tx();
|
||||||
|
let hash_index = self.hash_index.clone();
|
||||||
|
let uid_index = self.uid_index.clone();
|
||||||
|
let folder_path = folder.path().to_string();
|
||||||
|
let folder_hash = folder.hash();
|
||||||
|
let connection = self.folder_connections[&folder_hash].clone();
|
||||||
|
let closure = move || {
|
||||||
|
let connection = connection.clone();
|
||||||
|
let tx = tx.clone();
|
||||||
|
let mut response = String::with_capacity(8 * 1024);
|
||||||
|
{
|
||||||
|
let mut conn = connection.lock().unwrap();
|
||||||
|
|
||||||
|
debug!("locked for get {}", folder_path);
|
||||||
|
exit_on_error!(&tx,
|
||||||
|
conn.send_command(format!("EXAMINE {}", folder_path).as_bytes())
|
||||||
|
conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let examine_response = protocol_parser::select_response(&response)
|
||||||
|
.to_full_result()
|
||||||
|
.map_err(MeliError::from);
|
||||||
|
exit_on_error!(&tx, examine_response);
|
||||||
|
let mut exists: usize = match examine_response.unwrap() {
|
||||||
|
SelectResponse::Ok(ok) => ok.exists,
|
||||||
|
SelectResponse::Bad(b) => b.exists,
|
||||||
|
};
|
||||||
|
|
||||||
|
while exists > 1 {
|
||||||
|
let mut envelopes = vec![];
|
||||||
|
{
|
||||||
|
let mut conn = connection.lock().unwrap();
|
||||||
|
exit_on_error!(&tx,
|
||||||
|
conn.send_command(format!("UID FETCH {}:{} (FLAGS RFC822.HEADER)", std::cmp::max(exists.saturating_sub(10000), 1), exists).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) => {
|
||||||
|
debug!("responses len is {}", v.len());
|
||||||
|
for (uid, flags, b) in v {
|
||||||
|
if let Ok(e) = Envelope::from_bytes(&b, flags) {
|
||||||
|
hash_index
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(e.hash(), (uid, folder_hash));
|
||||||
|
uid_index.lock().unwrap().insert(uid, e.hash());
|
||||||
|
envelopes.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!(&e);
|
||||||
|
tx.send(AsyncStatus::Payload(Err(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exists = std::cmp::max(exists.saturating_sub(10000), 1);
|
||||||
|
debug!("sending payload");
|
||||||
|
tx.send(AsyncStatus::Payload(Ok(envelopes)));
|
||||||
|
}
|
||||||
|
tx.send(AsyncStatus::Finished);
|
||||||
|
};
|
||||||
|
Box::new(closure)
|
||||||
|
};
|
||||||
|
w.build(handle)
|
||||||
}
|
}
|
||||||
fn watch(&self, _sender: RefreshEventConsumer, _folders: &[Folder]) -> () {
|
|
||||||
unimplemented!();
|
fn watch(&self, sender: RefreshEventConsumer) -> Result<()> {
|
||||||
|
macro_rules! exit_on_error {
|
||||||
|
($sender:expr, $folder_hash:ident, $($result:expr)+) => {
|
||||||
|
$(if let Err(e) = $result {
|
||||||
|
debug!("failure: {}", e.to_string());
|
||||||
|
$sender.send(RefreshEvent {
|
||||||
|
hash: $folder_hash,
|
||||||
|
kind: RefreshEventKind::Failure(e),
|
||||||
|
});
|
||||||
|
std::process::exit(1);
|
||||||
|
})+
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let has_idle: bool = self.capabilities.contains(&b"IDLE"[0..]);
|
||||||
|
let sender = Arc::new(sender);
|
||||||
|
for f in self.folders.values() {
|
||||||
|
let mut conn = self.new_connection();
|
||||||
|
let main_conn = self.connection.clone();
|
||||||
|
let f_path = f.path().to_string();
|
||||||
|
let hash_index = self.hash_index.clone();
|
||||||
|
let uid_index = self.uid_index.clone();
|
||||||
|
let folder_hash = f.hash();
|
||||||
|
let sender = sender.clone();
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name(format!(
|
||||||
|
"{},{}: imap connection",
|
||||||
|
self.account_name.as_str(),
|
||||||
|
f_path.as_str()
|
||||||
|
))
|
||||||
|
.spawn(move || {
|
||||||
|
let mut response = String::with_capacity(8 * 1024);
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
conn.read_response(&mut response)
|
||||||
|
conn.send_command(format!("SELECT {}", f_path).as_bytes())
|
||||||
|
conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
debug!("select response {}", &response);
|
||||||
|
let mut prev_exists = match protocol_parser::select_response(&response)
|
||||||
|
.to_full_result()
|
||||||
|
.map_err(MeliError::from)
|
||||||
|
{
|
||||||
|
Ok(SelectResponse::Bad(bad)) => {
|
||||||
|
debug!(bad);
|
||||||
|
panic!("could not select mailbox");
|
||||||
|
}
|
||||||
|
Ok(SelectResponse::Ok(ok)) => {
|
||||||
|
debug!(&ok);
|
||||||
|
ok.exists
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("{:?}", e);
|
||||||
|
panic!("could not select mailbox");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if has_idle {
|
||||||
|
exit_on_error!(sender.as_ref(), folder_hash, conn.send_command(b"IDLE"));
|
||||||
|
let mut iter = ImapBlockingConnection::from(conn);
|
||||||
|
let mut beat = std::time::Instant::now();
|
||||||
|
let _26_mins = std::time::Duration::from_secs(26 * 60);
|
||||||
|
while let Some(line) = iter.next() {
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
if now.duration_since(beat) >= _26_mins {
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.set_nonblocking(true)
|
||||||
|
iter.conn.send_raw(b"DONE")
|
||||||
|
iter.conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.send_command(b"IDLE")
|
||||||
|
iter.conn.set_nonblocking(false)
|
||||||
|
);
|
||||||
|
{
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
main_conn.lock().unwrap().send_command(b"NOOP")
|
||||||
|
main_conn.lock().unwrap().read_response(&mut response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
beat = now;
|
||||||
|
}
|
||||||
|
match protocol_parser::untagged_responses(line.as_slice())
|
||||||
|
.to_full_result()
|
||||||
|
.map_err(MeliError::from)
|
||||||
|
{
|
||||||
|
Ok(Some(Recent(_))) => {
|
||||||
|
/* UID SEARCH RECENT */
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.set_nonblocking(true)
|
||||||
|
iter.conn.send_raw(b"DONE")
|
||||||
|
iter.conn.read_response(&mut response)
|
||||||
|
iter.conn.send_command(b"UID SEARCH RECENT")
|
||||||
|
iter.conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
match protocol_parser::search_results_raw(response.as_bytes())
|
||||||
|
.to_full_result()
|
||||||
|
.map_err(MeliError::from)
|
||||||
|
{
|
||||||
|
Ok(&[]) => {
|
||||||
|
debug!("UID SEARCH RECENT returned no results");
|
||||||
|
}
|
||||||
|
Ok(v) => {
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.send_command(
|
||||||
|
&[b"UID FETCH", v, b"(FLAGS RFC822.HEADER)"]
|
||||||
|
.join(&b' '),
|
||||||
|
)
|
||||||
|
iter.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) => {
|
||||||
|
for (uid, flags, b) in v {
|
||||||
|
if let Ok(env) =
|
||||||
|
Envelope::from_bytes(&b, flags)
|
||||||
|
{
|
||||||
|
hash_index.lock().unwrap().insert(
|
||||||
|
env.hash(),
|
||||||
|
(uid, folder_hash),
|
||||||
|
);
|
||||||
|
uid_index
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(uid, env.hash());
|
||||||
|
debug!(
|
||||||
|
"Create event {} {} {}",
|
||||||
|
env.hash(),
|
||||||
|
env.subject(),
|
||||||
|
f_path.as_str()
|
||||||
|
);
|
||||||
|
sender.send(RefreshEvent {
|
||||||
|
hash: folder_hash,
|
||||||
|
kind: Create(Box::new(env)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!(
|
||||||
|
"UID SEARCH RECENT err: {}\nresp: {}",
|
||||||
|
e.to_string(),
|
||||||
|
&response
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.send_command(b"IDLE")
|
||||||
|
iter.conn.set_nonblocking(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Some(Expunge(n))) => {
|
||||||
|
debug!("expunge {}", n);
|
||||||
|
}
|
||||||
|
Ok(Some(Exists(n))) => {
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.set_nonblocking(true)
|
||||||
|
iter.conn.send_raw(b"DONE")
|
||||||
|
iter.conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
/* UID FETCH ALL UID, cross-ref, then FETCH difference headers
|
||||||
|
* */
|
||||||
|
debug!("exists {}", n);
|
||||||
|
if n > prev_exists {
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.send_command(
|
||||||
|
&[
|
||||||
|
b"FETCH",
|
||||||
|
format!("{}:{}", prev_exists + 1, n).as_bytes(),
|
||||||
|
b"(UID FLAGS RFC822.HEADER)",
|
||||||
|
]
|
||||||
|
.join(&b' '),
|
||||||
|
)
|
||||||
|
iter.conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
match protocol_parser::uid_fetch_response(
|
||||||
|
response.as_bytes(),
|
||||||
|
)
|
||||||
|
.to_full_result()
|
||||||
|
.map_err(MeliError::from)
|
||||||
|
{
|
||||||
|
Ok(v) => {
|
||||||
|
for (uid, flags, b) in v {
|
||||||
|
if let Ok(env) = Envelope::from_bytes(&b, flags)
|
||||||
|
{
|
||||||
|
hash_index
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(env.hash(), (uid, folder_hash));
|
||||||
|
uid_index
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(uid, env.hash());
|
||||||
|
debug!(
|
||||||
|
"Create event {} {} {}",
|
||||||
|
env.hash(),
|
||||||
|
env.subject(),
|
||||||
|
f_path.as_str()
|
||||||
|
);
|
||||||
|
sender.send(RefreshEvent {
|
||||||
|
hash: folder_hash,
|
||||||
|
kind: Create(Box::new(env)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_exists = n;
|
||||||
|
} else if n < prev_exists {
|
||||||
|
prev_exists = n;
|
||||||
|
}
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
iter.conn.send_command(b"IDLE")
|
||||||
|
iter.conn.set_nonblocking(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(None) | Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("failure");
|
||||||
|
sender.send(RefreshEvent {
|
||||||
|
hash: folder_hash,
|
||||||
|
kind: RefreshEventKind::Failure(MeliError::new("conn_error")),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
main_conn.lock().unwrap().send_command(b"NOOP")
|
||||||
|
main_conn.lock().unwrap().read_response(&mut response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
exit_on_error!(
|
||||||
|
sender.as_ref(),
|
||||||
|
folder_hash,
|
||||||
|
conn.send_command(b"NOOP")
|
||||||
|
conn.read_response(&mut response)
|
||||||
|
);
|
||||||
|
for r in response.lines() {
|
||||||
|
// FIXME mimic IDLE
|
||||||
|
debug!(&r);
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(10 * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn folders(&self) -> FnvHashMap<FolderHash, Folder> {
|
||||||
|
if !self.folders.is_empty() {
|
||||||
|
return self
|
||||||
|
.folders
|
||||||
|
.iter()
|
||||||
|
.map(|(h, f)| (*h, f.clone() as Folder))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut folders: FnvHashMap<FolderHash, ImapFolder> = Default::default();
|
||||||
|
let mut res = String::with_capacity(8 * 1024);
|
||||||
|
let mut conn = self.connection.lock().unwrap();
|
||||||
|
conn.send_command(b"LIST \"\" \"*\"").unwrap();
|
||||||
|
conn.read_response(&mut res).unwrap();
|
||||||
|
debug!("out: {}", &res);
|
||||||
|
for l in res.lines().map(|l| l.trim()) {
|
||||||
|
if let Ok(mut folder) =
|
||||||
|
protocol_parser::list_folder_result(l.as_bytes()).to_full_result()
|
||||||
|
{
|
||||||
|
if let Some(parent) = folder.parent {
|
||||||
|
if folders.contains_key(&parent) {
|
||||||
|
folders
|
||||||
|
.entry(parent)
|
||||||
|
.and_modify(|e| e.children.push(folder.hash));
|
||||||
|
} else {
|
||||||
|
/* Insert dummy parent entry, populating only the children field. Later
|
||||||
|
* when we encounter the parent entry we will swap its children with
|
||||||
|
* dummy's */
|
||||||
|
folders.insert(
|
||||||
|
parent,
|
||||||
|
ImapFolder {
|
||||||
|
children: vec![folder.hash],
|
||||||
|
..ImapFolder::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if folders.contains_key(&folder.hash) {
|
||||||
|
let entry = folders.entry(folder.hash).or_default();
|
||||||
|
std::mem::swap(&mut entry.children, &mut folder.children);
|
||||||
|
std::mem::swap(entry, &mut folder);
|
||||||
|
} else {
|
||||||
|
folders.insert(folder.hash, folder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("parse error for {:?}", l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!(&folders);
|
||||||
|
folders
|
||||||
|
.iter()
|
||||||
|
.map(|(h, f)| (*h, f.clone() as Folder))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operation(&self, hash: EnvelopeHash, _folder_hash: FolderHash) -> Box<BackendOp> {
|
||||||
|
let (uid, folder_hash) = self.hash_index.lock().unwrap()[&hash];
|
||||||
|
Box::new(ImapOp::new(
|
||||||
|
uid,
|
||||||
|
self.folders[&folder_hash].path().to_string(),
|
||||||
|
self.connection.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
|
||||||
|
let path = self
|
||||||
|
.folders
|
||||||
|
.values()
|
||||||
|
.find(|v| v.name == folder)
|
||||||
|
.ok_or(MeliError::new(""))?;
|
||||||
|
let mut response = String::with_capacity(8 * 1024);
|
||||||
|
let mut conn = self.connection.lock().unwrap();
|
||||||
|
let flags = flags.unwrap_or(Flag::empty());
|
||||||
|
conn.send_command(
|
||||||
|
format!(
|
||||||
|
"APPEND \"{}\" ({}) {{{}}}",
|
||||||
|
path.path(),
|
||||||
|
flags_to_imap_list!(flags),
|
||||||
|
bytes.len()
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)?;
|
||||||
|
// wait for "+ Ready for literal data" reply
|
||||||
|
conn.wait_for_continuation_request()?;
|
||||||
|
conn.send_literal(bytes)?;
|
||||||
|
conn.read_response(&mut response)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_ipv4(host: &str, port: u16) -> Result<SocketAddr> {
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
|
||||||
|
let addrs = (host, port).to_socket_addrs()?;
|
||||||
|
for addr in addrs {
|
||||||
|
if let SocketAddr::V4(_) = addr {
|
||||||
|
return Ok(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(MeliError::new("Cannot lookup address"))
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! get_conf_val {
|
||||||
|
($s:ident[$var:literal]) => {
|
||||||
|
$s.extra.get($var).unwrap_or_else(|| {
|
||||||
|
eprintln!(
|
||||||
|
"IMAP connection for {} requires the field `{}` set",
|
||||||
|
$s.name.as_str(),
|
||||||
|
$var
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImapType {
|
impl ImapType {
|
||||||
pub fn new(_path: &str) -> Self {
|
pub fn new(s: &AccountSettings) -> Self {
|
||||||
ImapType {}
|
use std::io::prelude::*;
|
||||||
|
use std::net::TcpStream;
|
||||||
|
debug!(s);
|
||||||
|
let path = get_conf_val!(s["server_hostname"]);
|
||||||
|
|
||||||
|
let connector = TlsConnector::builder();
|
||||||
|
let connector = connector.build().unwrap();
|
||||||
|
|
||||||
|
let addr = if let Ok(a) = lookup_ipv4(path, 143) {
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
eprintln!("Could not lookup address {}", &path);
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut socket = TcpStream::connect(&addr).unwrap();
|
||||||
|
let cmd_id = 0;
|
||||||
|
socket
|
||||||
|
.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
let mut buf = vec![0; 1024];
|
||||||
|
let mut response = String::with_capacity(1024);
|
||||||
|
let mut cap_flag = false;
|
||||||
|
loop {
|
||||||
|
let len = socket.read(&mut buf).unwrap();
|
||||||
|
response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) });
|
||||||
|
if !cap_flag {
|
||||||
|
if response.starts_with("* OK [CAPABILITY") && response.find("\r\n").is_some() {
|
||||||
|
if let Some(pos) = response.as_bytes().find(b"\r\n") {
|
||||||
|
response.drain(0..pos + 2);
|
||||||
|
cap_flag = true;
|
||||||
|
}
|
||||||
|
} else if response.starts_with("* OK ") && response.find("\r\n").is_some() {
|
||||||
|
if let Some(pos) = response.as_bytes().find(b"\r\n") {
|
||||||
|
response.drain(0..pos + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cap_flag && response == "M0 OK Begin TLS negotiation now.\r\n" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.expect("set_nonblocking call failed");
|
||||||
|
socket
|
||||||
|
.set_read_timeout(Some(std::time::Duration::new(120, 0)))
|
||||||
|
.unwrap();
|
||||||
|
let stream = {
|
||||||
|
let mut conn_result = connector.connect(path, socket);
|
||||||
|
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) = conn_result {
|
||||||
|
let mut midhandshake_stream = Some(midhandshake_stream);
|
||||||
|
loop {
|
||||||
|
match midhandshake_stream.take().unwrap().handshake() {
|
||||||
|
Ok(r) => {
|
||||||
|
conn_result = Ok(r);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(native_tls::HandshakeError::WouldBlock(stream)) => {
|
||||||
|
midhandshake_stream = Some(stream);
|
||||||
|
}
|
||||||
|
p => {
|
||||||
|
p.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn_result.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut m = ImapType {
|
||||||
|
account_name: s.name().to_string(),
|
||||||
|
server_hostname: get_conf_val!(s["server_hostname"]).to_string(),
|
||||||
|
server_username: get_conf_val!(s["server_username"]).to_string(),
|
||||||
|
server_password: get_conf_val!(s["server_password"]).to_string(),
|
||||||
|
folders: Default::default(),
|
||||||
|
connection: Arc::new(Mutex::new(ImapConnection { cmd_id, stream })),
|
||||||
|
folder_connections: Default::default(),
|
||||||
|
hash_index: Default::default(),
|
||||||
|
uid_index: Default::default(),
|
||||||
|
capabilities: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut conn = m.connection.lock().unwrap();
|
||||||
|
conn.send_command(
|
||||||
|
format!(
|
||||||
|
"LOGIN \"{}\" \"{}\"",
|
||||||
|
get_conf_val!(s["server_username"]),
|
||||||
|
get_conf_val!(s["server_password"])
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut res = String::with_capacity(8 * 1024);
|
||||||
|
conn.read_lines(&mut res, String::new()).unwrap();
|
||||||
|
std::io::stderr().write(res.as_bytes()).unwrap();
|
||||||
|
m.capabilities = match protocol_parser::capabilities(res.as_bytes())
|
||||||
|
.to_full_result()
|
||||||
|
.map_err(MeliError::from)
|
||||||
|
{
|
||||||
|
Ok(c) => {
|
||||||
|
eprintln!("cap len {}", c.len());
|
||||||
|
|
||||||
|
FnvHashSet::from_iter(c.into_iter().map(|s| s.to_vec()))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Could not login in account `{}`: {}",
|
||||||
|
m.account_name.as_str(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!(m
|
||||||
|
.capabilities
|
||||||
|
.iter()
|
||||||
|
.map(|s| String::from_utf8(s.to_vec()).unwrap())
|
||||||
|
.collect::<Vec<String>>());
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
m.folders = m.imap_folders();
|
||||||
|
for f in m.folders.keys() {
|
||||||
|
m.folder_connections
|
||||||
|
.insert(*f, Arc::new(Mutex::new(m.new_connection())));
|
||||||
|
}
|
||||||
|
m
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
|
pub fn shell(&mut self) {
|
||||||
|
self.folders();
|
||||||
|
let mut conn = self.connection.lock().unwrap();
|
||||||
|
let mut res = String::with_capacity(8 * 1024);
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
loop {
|
||||||
|
use std::io;
|
||||||
|
input.clear();
|
||||||
|
|
||||||
|
match io::stdin().read_line(&mut input) {
|
||||||
|
Ok(_) => {
|
||||||
|
conn.send_command(input.as_bytes()).unwrap();
|
||||||
|
conn.read_response(&mut res).unwrap();
|
||||||
|
debug!("out: {}", &res);
|
||||||
|
if input.trim().eq_ignore_ascii_case("logout") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => debug!("error: {}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_connection(&self) -> ImapConnection {
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::net::TcpStream;
|
||||||
|
let path = &self.server_hostname;
|
||||||
|
|
||||||
|
let connector = TlsConnector::builder();
|
||||||
|
let connector = connector.build().unwrap();
|
||||||
|
|
||||||
|
let addr = if let Ok(a) = lookup_ipv4(path, 143) {
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
eprintln!("Could not lookup address {}", &path);
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut socket = TcpStream::connect(&addr).unwrap();
|
||||||
|
let cmd_id = 0;
|
||||||
|
socket
|
||||||
|
.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut buf = vec![0; 1024];
|
||||||
|
let mut response = String::with_capacity(1024);
|
||||||
|
let mut cap_flag = false;
|
||||||
|
loop {
|
||||||
|
let len = socket.read(&mut buf)?;
|
||||||
|
response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) });
|
||||||
|
if !cap_flag {
|
||||||
|
if response.starts_with("* OK [CAPABILITY") && response.find("\r\n").is_some() {
|
||||||
|
if let Some(pos) = response.as_bytes().find(b"\r\n") {
|
||||||
|
response.drain(0..pos + 2);
|
||||||
|
cap_flag = true;
|
||||||
|
}
|
||||||
|
} else if response.starts_with("* OK ") && response.find("\r\n").is_some() {
|
||||||
|
if let Some(pos) = response.as_bytes().find(b"\r\n") {
|
||||||
|
response.drain(0..pos + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cap_flag && response == "M0 OK Begin TLS negotiation now.\r\n" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.expect("set_nonblocking call failed");
|
||||||
|
socket
|
||||||
|
.set_read_timeout(Some(std::time::Duration::new(120, 0)))
|
||||||
|
.unwrap();
|
||||||
|
let stream = {
|
||||||
|
let mut conn_result = connector.connect(path, socket);
|
||||||
|
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) = conn_result {
|
||||||
|
let mut midhandshake_stream = Some(midhandshake_stream);
|
||||||
|
loop {
|
||||||
|
match midhandshake_stream.take().unwrap().handshake() {
|
||||||
|
Ok(r) => {
|
||||||
|
conn_result = Ok(r);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(native_tls::HandshakeError::WouldBlock(stream)) => {
|
||||||
|
midhandshake_stream = Some(stream);
|
||||||
|
}
|
||||||
|
p => {
|
||||||
|
p.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn_result.unwrap()
|
||||||
|
};
|
||||||
|
let mut ret = ImapConnection { cmd_id, stream };
|
||||||
|
ret.send_command(
|
||||||
|
format!(
|
||||||
|
"LOGIN \"{}\" \"{}\"",
|
||||||
|
&self.server_username, &self.server_password
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn imap_folders(&self) -> FnvHashMap<FolderHash, ImapFolder> {
|
||||||
|
let mut folders: FnvHashMap<FolderHash, ImapFolder> = Default::default();
|
||||||
|
let mut res = String::with_capacity(8 * 1024);
|
||||||
|
let mut conn = self.connection.lock().unwrap();
|
||||||
|
conn.send_command(b"LIST \"\" \"*\"").unwrap();
|
||||||
|
conn.read_response(&mut res).unwrap();
|
||||||
|
debug!("out: {}", &res);
|
||||||
|
for l in res.lines().map(|l| l.trim()) {
|
||||||
|
if let Ok(mut folder) =
|
||||||
|
protocol_parser::list_folder_result(l.as_bytes()).to_full_result()
|
||||||
|
{
|
||||||
|
if let Some(parent) = folder.parent {
|
||||||
|
if folders.contains_key(&parent) {
|
||||||
|
folders
|
||||||
|
.entry(parent)
|
||||||
|
.and_modify(|e| e.children.push(folder.hash));
|
||||||
|
} else {
|
||||||
|
/* Insert dummy parent entry, populating only the children field. Later
|
||||||
|
* when we encounter the parent entry we will swap its children with
|
||||||
|
* dummy's */
|
||||||
|
folders.insert(
|
||||||
|
parent,
|
||||||
|
ImapFolder {
|
||||||
|
children: vec![folder.hash],
|
||||||
|
..ImapFolder::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if folders.contains_key(&folder.hash) {
|
||||||
|
let entry = folders.entry(folder.hash).or_default();
|
||||||
|
std::mem::swap(&mut entry.children, &mut folder.children);
|
||||||
|
*entry = folder;
|
||||||
|
} else {
|
||||||
|
folders.insert(folder.hash, folder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("parse error for {:?}", l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!(folders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -26,7 +26,7 @@ use super::{
|
||||||
use super::{MaildirFolder, MaildirOp};
|
use super::{MaildirFolder, MaildirOp};
|
||||||
use crate::async_workers::*;
|
use crate::async_workers::*;
|
||||||
use crate::conf::AccountSettings;
|
use crate::conf::AccountSettings;
|
||||||
use crate::email::{Envelope, EnvelopeHash};
|
use crate::email::{Envelope, EnvelopeHash, Flag};
|
||||||
use crate::error::{MeliError, Result};
|
use crate::error::{MeliError, Result};
|
||||||
|
|
||||||
extern crate notify;
|
extern crate notify;
|
||||||
|
@ -464,7 +464,7 @@ impl MailBackend for MaildirType {
|
||||||
Box::new(MaildirOp::new(hash, self.hash_indexes.clone(), folder_hash))
|
Box::new(MaildirOp::new(hash, self.hash_indexes.clone(), folder_hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&self, bytes: &[u8], folder: &str) -> Result<()> {
|
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
|
||||||
for f in self.folders.values() {
|
for f in self.folders.values() {
|
||||||
if f.name == folder {
|
if f.name == folder {
|
||||||
let mut path = f.fs_path.clone();
|
let mut path = f.fs_path.clone();
|
||||||
|
@ -484,13 +484,34 @@ impl MailBackend for MaildirType {
|
||||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_millis();
|
.as_millis();
|
||||||
path.push(&format!(
|
let mut filename = format!(
|
||||||
"{}.{:x}_{}.{}:2,",
|
"{}.{:x}_{}.{}:2,",
|
||||||
timestamp,
|
timestamp,
|
||||||
u128::from_be_bytes(rand_buf),
|
u128::from_be_bytes(rand_buf),
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
hostn_buf.trim()
|
hostn_buf.trim()
|
||||||
));
|
);
|
||||||
|
if let Some(flags) = flags {
|
||||||
|
if !(flags & Flag::DRAFT).is_empty() {
|
||||||
|
filename.push('D');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::FLAGGED).is_empty() {
|
||||||
|
filename.push('F');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::PASSED).is_empty() {
|
||||||
|
filename.push('P');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::REPLIED).is_empty() {
|
||||||
|
filename.push('R');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::SEEN).is_empty() {
|
||||||
|
filename.push('S');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::TRASHED).is_empty() {
|
||||||
|
filename.push('T');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.push(filename);
|
||||||
}
|
}
|
||||||
debug!("saving at {}", path.display());
|
debug!("saving at {}", path.display());
|
||||||
let file = fs::File::create(path).unwrap();
|
let file = fs::File::create(path).unwrap();
|
||||||
|
|
|
@ -262,7 +262,7 @@ pub fn mbox_parse(
|
||||||
while !input.is_empty() {
|
while !input.is_empty() {
|
||||||
let next_offset: Option<usize> = input.find(b"\n\nFrom ");
|
let next_offset: Option<usize> = input.find(b"\n\nFrom ");
|
||||||
if let Some(len) = next_offset {
|
if let Some(len) = next_offset {
|
||||||
match Envelope::from_bytes(&input[..len]) {
|
match Envelope::from_bytes(&input[..len], None) {
|
||||||
Ok(mut env) => {
|
Ok(mut env) => {
|
||||||
let mut flags = Flag::empty();
|
let mut flags = Flag::empty();
|
||||||
if env.other_headers().contains_key("Status") {
|
if env.other_headers().contains_key("Status") {
|
||||||
|
@ -307,7 +307,7 @@ pub fn mbox_parse(
|
||||||
offset += len + 2;
|
offset += len + 2;
|
||||||
input = &input[len + 2..];
|
input = &input[len + 2..];
|
||||||
} else {
|
} else {
|
||||||
match Envelope::from_bytes(input) {
|
match Envelope::from_bytes(input, None) {
|
||||||
Ok(mut env) => {
|
Ok(mut env) => {
|
||||||
let mut flags = Flag::empty();
|
let mut flags = Flag::empty();
|
||||||
if env.other_headers().contains_key("Status") {
|
if env.other_headers().contains_key("Status") {
|
||||||
|
@ -534,7 +534,7 @@ impl MailBackend for MboxType {
|
||||||
Box::new(MboxOp::new(hash, self.path.as_path(), offset, length))
|
Box::new(MboxOp::new(hash, self.path.as_path(), offset, length))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&self, bytes: &[u8], folder: &str) -> Result<()> {
|
fn save(&self, _bytes: &[u8], _folder: &str, _flags: Option<Flag>) -> Result<()> {
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
use std::collections::hash_map::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Default, Clone)]
|
#[derive(Debug, Serialize, Default, Clone)]
|
||||||
pub struct AccountSettings {
|
pub struct AccountSettings {
|
||||||
|
@ -29,6 +30,8 @@ pub struct AccountSettings {
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub subscribed_folders: Vec<String>,
|
pub subscribed_folders: Vec<String>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub extra: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountSettings {
|
impl AccountSettings {
|
||||||
|
|
|
@ -280,7 +280,7 @@ impl Deref for EnvelopeWrapper {
|
||||||
impl EnvelopeWrapper {
|
impl EnvelopeWrapper {
|
||||||
pub fn new(buffer: Vec<u8>) -> Result<Self> {
|
pub fn new(buffer: Vec<u8>) -> Result<Self> {
|
||||||
Ok(EnvelopeWrapper {
|
Ok(EnvelopeWrapper {
|
||||||
envelope: Envelope::from_bytes(&buffer)?,
|
envelope: Envelope::from_bytes(&buffer, None)?,
|
||||||
buffer,
|
buffer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -373,12 +373,15 @@ impl Envelope {
|
||||||
self.hash = new_hash;
|
self.hash = new_hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<Envelope> {
|
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Envelope> {
|
||||||
let mut h = DefaultHasher::new();
|
let mut h = DefaultHasher::new();
|
||||||
h.write(bytes);
|
h.write(bytes);
|
||||||
let mut e = Envelope::new(h.finish());
|
let mut e = Envelope::new(h.finish());
|
||||||
let res = e.populate_headers(bytes).ok();
|
let res = e.populate_headers(bytes).ok();
|
||||||
if res.is_some() {
|
if res.is_some() {
|
||||||
|
if let Some(f) = flags {
|
||||||
|
e.flags = f;
|
||||||
|
}
|
||||||
return Ok(e);
|
return Ok(e);
|
||||||
}
|
}
|
||||||
Err(MeliError::new("Couldn't parse mail."))
|
Err(MeliError::new("Couldn't parse mail."))
|
||||||
|
|
|
@ -164,6 +164,10 @@ impl AttachmentBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subattachments(raw: &[u8], boundary: &[u8]) -> Vec<Attachment> {
|
pub fn subattachments(raw: &[u8], boundary: &[u8]) -> Vec<Attachment> {
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
match parser::attachments(raw, boundary).to_full_result() {
|
match parser::attachments(raw, boundary).to_full_result() {
|
||||||
Ok(attachments) => {
|
Ok(attachments) => {
|
||||||
let mut vec = Vec::with_capacity(attachments.len());
|
let mut vec = Vec::with_capacity(attachments.len());
|
||||||
|
|
|
@ -119,6 +119,11 @@ fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|| i + 1 == input_len)
|
|| i + 1 == input_len)
|
||||||
{
|
{
|
||||||
return IResult::Done(&input[(i + 1)..], &input[0..i]);
|
return IResult::Done(&input[(i + 1)..], &input[0..i]);
|
||||||
|
} else if input[i..].starts_with(b"\r\n")
|
||||||
|
&& (((i + 2) < input_len && input[i + 2] != b' ' && input[i + 2] != b'\t')
|
||||||
|
|| i + 2 == input_len)
|
||||||
|
{
|
||||||
|
return IResult::Done(&input[(i + 2)..], &input[0..i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IResult::Incomplete(Needed::Unknown)
|
IResult::Incomplete(Needed::Unknown)
|
||||||
|
@ -128,7 +133,7 @@ fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return IResult::Incomplete(Needed::Unknown);
|
return IResult::Incomplete(Needed::Unknown);
|
||||||
} else if input.starts_with(b"\n") {
|
} else if input.starts_with(b"\n") || input.starts_with(b"\r\n") {
|
||||||
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
||||||
}
|
}
|
||||||
let mut ptr = 0;
|
let mut ptr = 0;
|
||||||
|
@ -152,6 +157,11 @@ fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||||
if ptr >= input.len() {
|
if ptr >= input.len() {
|
||||||
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
||||||
}
|
}
|
||||||
|
} else if input[ptr..].starts_with(b"\r\n") {
|
||||||
|
ptr += 2;
|
||||||
|
if ptr > input.len() {
|
||||||
|
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
while input[ptr] == b' ' || input[ptr] == b'\t' {
|
while input[ptr] == b' ' || input[ptr] == b'\t' {
|
||||||
ptr += 1;
|
ptr += 1;
|
||||||
|
@ -169,13 +179,17 @@ fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||||
fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return IResult::Incomplete(Needed::Unknown);
|
return IResult::Incomplete(Needed::Unknown);
|
||||||
} else if input.starts_with(b"\n") {
|
} else if input.starts_with(b"\n") || input.starts_with(b"\r\n") {
|
||||||
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
return IResult::Error(error_code!(ErrorKind::Custom(43)));
|
||||||
}
|
}
|
||||||
let mut ptr = 0;
|
let mut ptr = 0;
|
||||||
let mut name: &[u8] = &input[0..0];
|
let mut name: &[u8] = &input[0..0];
|
||||||
for (i, x) in input.iter().enumerate() {
|
for (i, x) in input.iter().enumerate() {
|
||||||
if *x == b':' || *x == b'\n' {
|
if input[i..].starts_with(b"\r\n") {
|
||||||
|
name = &input[0..i];
|
||||||
|
ptr = i + 2;
|
||||||
|
break;
|
||||||
|
} else if *x == b':' || *x == b'\n' {
|
||||||
name = &input[0..i];
|
name = &input[0..i];
|
||||||
ptr = i;
|
ptr = i;
|
||||||
break;
|
break;
|
||||||
|
@ -201,7 +215,17 @@ fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||||
if ptr >= input.len() {
|
if ptr >= input.len() {
|
||||||
return IResult::Incomplete(Needed::Unknown);
|
return IResult::Incomplete(Needed::Unknown);
|
||||||
}
|
}
|
||||||
if input[ptr] != b' ' && input[ptr] != b'\t' {
|
if input.len() > ptr && input[ptr] != b' ' && input[ptr] != b'\t' {
|
||||||
|
IResult::Done(&input[ptr..], (name, b""))
|
||||||
|
} else {
|
||||||
|
IResult::Error(error_code!(ErrorKind::Custom(43)))
|
||||||
|
}
|
||||||
|
} else if input[ptr..].starts_with(b"\r\n") {
|
||||||
|
ptr += 2;
|
||||||
|
if ptr > input.len() {
|
||||||
|
return IResult::Incomplete(Needed::Unknown);
|
||||||
|
}
|
||||||
|
if input.len() > ptr && input[ptr] != b' ' && input[ptr] != b'\t' {
|
||||||
IResult::Done(&input[ptr..], (name, b""))
|
IResult::Done(&input[ptr..], (name, b""))
|
||||||
} else {
|
} else {
|
||||||
IResult::Error(error_code!(ErrorKind::Custom(43)))
|
IResult::Error(error_code!(ErrorKind::Custom(43)))
|
||||||
|
@ -224,8 +248,10 @@ pub fn headers_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
return IResult::Incomplete(Needed::Unknown);
|
return IResult::Incomplete(Needed::Unknown);
|
||||||
}
|
}
|
||||||
for (i, x) in input.iter().enumerate() {
|
for (i, x) in input.iter().enumerate() {
|
||||||
if *x == b'\n' && i + 1 < input.len() && input[i + 1] == b'\n' {
|
if input[i..].starts_with(b"\n\n") {
|
||||||
return IResult::Done(&input[(i + 1)..], &input[0..=i]);
|
return IResult::Done(&input[(i + 1)..], &input[0..=i]);
|
||||||
|
} else if input[i..].starts_with(b"\r\n\r\n") {
|
||||||
|
return IResult::Done(&input[(i + 2)..], &input[0..=i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IResult::Error(error_code!(ErrorKind::Custom(43)))
|
IResult::Error(error_code!(ErrorKind::Custom(43)))
|
||||||
|
@ -233,12 +259,12 @@ pub fn headers_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
|
||||||
named!(pub body_raw<&[u8]>,
|
named!(pub body_raw<&[u8]>,
|
||||||
do_parse!(
|
do_parse!(
|
||||||
take_until1!("\n\n") >>
|
alt_complete!(take_until1!("\n\n") | take_until1!("\r\n\r\n")) >>
|
||||||
body: take_while!(call!(|_| true)) >>
|
body: take_while!(call!(|_| true)) >>
|
||||||
( { body } )));
|
( { body } )));
|
||||||
|
|
||||||
named!(pub mail<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>,
|
named!(pub mail<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>,
|
||||||
separated_pair!(headers, tag!(b"\n"), take_while!(call!(|_| true))));
|
separated_pair!(headers, alt_complete!(tag!(b"\n") | tag!(b"\r\n")), take_while!(call!(|_| true))));
|
||||||
named!(pub attachment<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>,
|
named!(pub attachment<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>,
|
||||||
do_parse!(
|
do_parse!(
|
||||||
opt!(is_a!(" \n\t\r")) >>
|
opt!(is_a!(" \n\t\r")) >>
|
||||||
|
@ -543,7 +569,7 @@ named!(pub address_list<String>, ws!(do_parse!(
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
list.iter().fold(String::with_capacity(string_len),
|
list.iter().fold(String::with_capacity(string_len),
|
||||||
|acc, x| {
|
|acc, x| {
|
||||||
let mut acc = acc + &String::from_utf8_lossy(x.replace(b"\n", b"").replace(b"\t", b" ").trim());
|
let mut acc = acc + &String::from_utf8_lossy(x.replace(b"\n", b"").replace(b"\r", b"").replace(b"\t", b" ").trim());
|
||||||
if i != list_len - 1 {
|
if i != list_len - 1 {
|
||||||
acc.push_str(" ");
|
acc.push_str(" ");
|
||||||
i+=1;
|
i+=1;
|
||||||
|
@ -626,7 +652,7 @@ fn attachments_f<'a>(input: &'a [u8], boundary: &[u8]) -> IResult<&'a [u8], Vec<
|
||||||
input = &input[b_start - 2..];
|
input = &input[b_start - 2..];
|
||||||
if &input[0..2] == b"--" {
|
if &input[0..2] == b"--" {
|
||||||
input = &input[2 + boundary.len()..];
|
input = &input[2 + boundary.len()..];
|
||||||
if &input[0..1] != b"\n" {
|
if input[0] != b'\n' && !input[0..].starts_with(b"\r\n") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
input = &input[1..];
|
input = &input[1..];
|
||||||
|
|
|
@ -12,6 +12,10 @@ path = "src/email_parse.rs"
|
||||||
name = "linebreak"
|
name = "linebreak"
|
||||||
path = "src/linebreak.rs"
|
path = "src/linebreak.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "imapconn"
|
||||||
|
path = "src/imap_conn.rs"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
melib = { path = "../melib", version = "*" }
|
melib = { path = "../melib", version = "*" }
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
|
@ -775,6 +775,7 @@ pub fn send_draft(context: &mut Context, account_cursor: usize, draft: Draft) ->
|
||||||
.settings
|
.settings
|
||||||
.conf()
|
.conf()
|
||||||
.sent_folder(),
|
.sent_folder(),
|
||||||
|
Some(Flag::SEEN),
|
||||||
) {
|
) {
|
||||||
debug!("{:?} could not save sent msg", e);
|
debug!("{:?} could not save sent msg", e);
|
||||||
context.replies.push_back(UIEvent::Notification(
|
context.replies.push_back(UIEvent::Notification(
|
||||||
|
|
|
@ -127,6 +127,8 @@ pub struct FileAccount {
|
||||||
sent_folder: String,
|
sent_folder: String,
|
||||||
draft_folder: String,
|
draft_folder: String,
|
||||||
identity: String,
|
identity: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub extra: HashMap<String, String>,
|
||||||
|
|
||||||
#[serde(default = "none")]
|
#[serde(default = "none")]
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
@ -161,6 +163,7 @@ impl From<FileAccount> for AccountConf {
|
||||||
read_only: x.read_only,
|
read_only: x.read_only,
|
||||||
display_name,
|
display_name,
|
||||||
subscribed_folders: x.subscribed_folders.clone(),
|
subscribed_folders: x.subscribed_folders.clone(),
|
||||||
|
extra: x.extra.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root_path = PathBuf::from(acc.root_folder.as_str());
|
let root_path = PathBuf::from(acc.root_folder.as_str());
|
||||||
|
|
|
@ -574,16 +574,16 @@ impl Account {
|
||||||
}
|
}
|
||||||
let finalize = draft.finalise()?;
|
let finalize = draft.finalise()?;
|
||||||
self.backend
|
self.backend
|
||||||
.save(&finalize.as_bytes(), &self.settings.conf.draft_folder)
|
.save(&finalize.as_bytes(), &self.settings.conf.draft_folder, None)
|
||||||
}
|
}
|
||||||
pub fn save(&self, bytes: &[u8], folder: &str) -> Result<()> {
|
pub fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
|
||||||
if self.settings.account.read_only() {
|
if self.settings.account.read_only() {
|
||||||
return Err(MeliError::new(format!(
|
return Err(MeliError::new(format!(
|
||||||
"Account {} is read-only.",
|
"Account {} is read-only.",
|
||||||
self.name.as_str()
|
self.name.as_str()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
self.backend.save(bytes, folder)
|
self.backend.save(bytes, folder, flags)
|
||||||
}
|
}
|
||||||
pub fn iter_mailboxes(&self) -> MailboxIterator {
|
pub fn iter_mailboxes(&self) -> MailboxIterator {
|
||||||
MailboxIterator {
|
MailboxIterator {
|
||||||
|
|
Loading…
Reference in New Issue