parent
3f7d962abd
commit
0812242f60
|
@ -19,6 +19,7 @@ notify = "4.0.1"
|
|||
notify-rust = "^3"
|
||||
termion = "1.5.1"
|
||||
xdg = "2.1.0"
|
||||
native-tls = "0.2"
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
bincode = "1.0.1"
|
||||
|
|
|
@ -22,6 +22,7 @@ pub mod imap;
|
|||
pub mod maildir;
|
||||
pub mod mbox;
|
||||
|
||||
pub use self::imap::ImapType;
|
||||
use crate::async_workers::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::error::{MeliError, Result};
|
||||
|
@ -63,7 +64,10 @@ impl Backends {
|
|||
"mbox".to_string(),
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -161,11 +165,10 @@ pub trait MailBackend: ::std::fmt::Debug {
|
|||
fn folders(&self) -> FnvHashMap<FolderHash, Folder>;
|
||||
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<()> {
|
||||
Ok(())
|
||||
}
|
||||
//login function
|
||||
}
|
||||
|
||||
/// 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.
|
||||
*
|
||||
|
@ -19,56 +19,816 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
use async::*;
|
||||
use error::Result;
|
||||
use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder};
|
||||
use mailbox::email::Envelope;
|
||||
#[macro_use]
|
||||
mod protocol_parser;
|
||||
pub use protocol_parser::{UntaggedResponse::*, *};
|
||||
mod folder;
|
||||
pub use folder::*;
|
||||
mod operations;
|
||||
pub use operations::*;
|
||||
mod connection;
|
||||
pub use connection::*;
|
||||
|
||||
/// `BackendOp` implementor for Imap
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ImapOp {}
|
||||
extern crate native_tls;
|
||||
|
||||
impl ImapOp {
|
||||
pub fn new(_path: String) -> Self {
|
||||
ImapOp {}
|
||||
}
|
||||
}
|
||||
use crate::async_workers::{Async, AsyncBuilder, AsyncStatus};
|
||||
use crate::backends::BackendOp;
|
||||
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)]
|
||||
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 {
|
||||
fn get(&self, _folder: &Folder) -> Async<Result<Vec<Envelope>>> {
|
||||
unimplemented!();
|
||||
fn get(&mut self, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
|
||||
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 {
|
||||
pub fn new(_path: &str) -> Self {
|
||||
ImapType {}
|
||||
pub fn new(s: &AccountSettings) -> Self {
|
||||
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 crate::async_workers::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::{Envelope, EnvelopeHash};
|
||||
use crate::email::{Envelope, EnvelopeHash, Flag};
|
||||
use crate::error::{MeliError, Result};
|
||||
|
||||
extern crate notify;
|
||||
|
@ -464,7 +464,7 @@ impl MailBackend for MaildirType {
|
|||
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() {
|
||||
if f.name == folder {
|
||||
let mut path = f.fs_path.clone();
|
||||
|
@ -484,13 +484,34 @@ impl MailBackend for MaildirType {
|
|||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
path.push(&format!(
|
||||
let mut filename = format!(
|
||||
"{}.{:x}_{}.{}:2,",
|
||||
timestamp,
|
||||
u128::from_be_bytes(rand_buf),
|
||||
std::process::id(),
|
||||
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());
|
||||
let file = fs::File::create(path).unwrap();
|
||||
|
|
|
@ -262,7 +262,7 @@ pub fn mbox_parse(
|
|||
while !input.is_empty() {
|
||||
let next_offset: Option<usize> = input.find(b"\n\nFrom ");
|
||||
if let Some(len) = next_offset {
|
||||
match Envelope::from_bytes(&input[..len]) {
|
||||
match Envelope::from_bytes(&input[..len], None) {
|
||||
Ok(mut env) => {
|
||||
let mut flags = Flag::empty();
|
||||
if env.other_headers().contains_key("Status") {
|
||||
|
@ -307,7 +307,7 @@ pub fn mbox_parse(
|
|||
offset += len + 2;
|
||||
input = &input[len + 2..];
|
||||
} else {
|
||||
match Envelope::from_bytes(input) {
|
||||
match Envelope::from_bytes(input, None) {
|
||||
Ok(mut env) => {
|
||||
let mut flags = Flag::empty();
|
||||
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))
|
||||
}
|
||||
|
||||
fn save(&self, bytes: &[u8], folder: &str) -> Result<()> {
|
||||
fn save(&self, _bytes: &[u8], _folder: &str, _flags: Option<Flag>) -> Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::collections::hash_map::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Default, Clone)]
|
||||
pub struct AccountSettings {
|
||||
|
@ -29,6 +30,8 @@ pub struct AccountSettings {
|
|||
pub read_only: bool,
|
||||
pub display_name: Option<String>,
|
||||
pub subscribed_folders: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl AccountSettings {
|
||||
|
|
|
@ -280,7 +280,7 @@ impl Deref for EnvelopeWrapper {
|
|||
impl EnvelopeWrapper {
|
||||
pub fn new(buffer: Vec<u8>) -> Result<Self> {
|
||||
Ok(EnvelopeWrapper {
|
||||
envelope: Envelope::from_bytes(&buffer)?,
|
||||
envelope: Envelope::from_bytes(&buffer, None)?,
|
||||
buffer,
|
||||
})
|
||||
}
|
||||
|
@ -373,12 +373,15 @@ impl Envelope {
|
|||
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();
|
||||
h.write(bytes);
|
||||
let mut e = Envelope::new(h.finish());
|
||||
let res = e.populate_headers(bytes).ok();
|
||||
if res.is_some() {
|
||||
if let Some(f) = flags {
|
||||
e.flags = f;
|
||||
}
|
||||
return Ok(e);
|
||||
}
|
||||
Err(MeliError::new("Couldn't parse mail."))
|
||||
|
|
|
@ -164,6 +164,10 @@ impl AttachmentBuilder {
|
|||
}
|
||||
|
||||
pub fn subattachments(raw: &[u8], boundary: &[u8]) -> Vec<Attachment> {
|
||||
if raw.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
match parser::attachments(raw, boundary).to_full_result() {
|
||||
Ok(attachments) => {
|
||||
let mut vec = Vec::with_capacity(attachments.len());
|
||||
|
|
|
@ -119,6 +119,11 @@ fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
|||
|| i + 1 == input_len)
|
||||
{
|
||||
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)
|
||||
|
@ -128,7 +133,7 @@ fn header_value(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
|||
fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||
if input.is_empty() {
|
||||
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)));
|
||||
}
|
||||
let mut ptr = 0;
|
||||
|
@ -152,6 +157,11 @@ fn header_with_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
|||
if ptr >= input.len() {
|
||||
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' {
|
||||
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])> {
|
||||
if input.is_empty() {
|
||||
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)));
|
||||
}
|
||||
let mut ptr = 0;
|
||||
let mut name: &[u8] = &input[0..0];
|
||||
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];
|
||||
ptr = i;
|
||||
break;
|
||||
|
@ -201,7 +215,17 @@ fn header_without_val(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
|||
if ptr >= input.len() {
|
||||
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""))
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
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]);
|
||||
} 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)))
|
||||
|
@ -233,12 +259,12 @@ pub fn headers_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
|||
|
||||
named!(pub body_raw<&[u8]>,
|
||||
do_parse!(
|
||||
take_until1!("\n\n") >>
|
||||
alt_complete!(take_until1!("\n\n") | take_until1!("\r\n\r\n")) >>
|
||||
body: take_while!(call!(|_| true)) >>
|
||||
( { body } )));
|
||||
|
||||
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])>,
|
||||
do_parse!(
|
||||
opt!(is_a!(" \n\t\r")) >>
|
||||
|
@ -543,7 +569,7 @@ named!(pub address_list<String>, ws!(do_parse!(
|
|||
let mut i = 0;
|
||||
list.iter().fold(String::with_capacity(string_len),
|
||||
|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 {
|
||||
acc.push_str(" ");
|
||||
i+=1;
|
||||
|
@ -626,7 +652,7 @@ fn attachments_f<'a>(input: &'a [u8], boundary: &[u8]) -> IResult<&'a [u8], Vec<
|
|||
input = &input[b_start - 2..];
|
||||
if &input[0..2] == b"--" {
|
||||
input = &input[2 + boundary.len()..];
|
||||
if &input[0..1] != b"\n" {
|
||||
if input[0] != b'\n' && !input[0..].starts_with(b"\r\n") {
|
||||
continue;
|
||||
}
|
||||
input = &input[1..];
|
||||
|
|
|
@ -12,6 +12,10 @@ path = "src/email_parse.rs"
|
|||
name = "linebreak"
|
||||
path = "src/linebreak.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "imapconn"
|
||||
path = "src/imap_conn.rs"
|
||||
|
||||
|
||||
[dependencies]
|
||||
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
|
||||
.conf()
|
||||
.sent_folder(),
|
||||
Some(Flag::SEEN),
|
||||
) {
|
||||
debug!("{:?} could not save sent msg", e);
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
|
|
|
@ -127,6 +127,8 @@ pub struct FileAccount {
|
|||
sent_folder: String,
|
||||
draft_folder: String,
|
||||
identity: String,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, String>,
|
||||
|
||||
#[serde(default = "none")]
|
||||
display_name: Option<String>,
|
||||
|
@ -161,6 +163,7 @@ impl From<FileAccount> for AccountConf {
|
|||
read_only: x.read_only,
|
||||
display_name,
|
||||
subscribed_folders: x.subscribed_folders.clone(),
|
||||
extra: x.extra.clone(),
|
||||
};
|
||||
|
||||
let root_path = PathBuf::from(acc.root_folder.as_str());
|
||||
|
|
|
@ -574,16 +574,16 @@ impl Account {
|
|||
}
|
||||
let finalize = draft.finalise()?;
|
||||
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() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Account {} is read-only.",
|
||||
self.name.as_str()
|
||||
)));
|
||||
}
|
||||
self.backend.save(bytes, folder)
|
||||
self.backend.save(bytes, folder, flags)
|
||||
}
|
||||
pub fn iter_mailboxes(&self) -> MailboxIterator {
|
||||
MailboxIterator {
|
||||
|
|
Loading…
Reference in New Issue