Add IMAP backend

TODOs: new message events (untagged responses)
embed
Manos Pitsidianakis 2019-06-28 19:34:40 +03:00
parent 3f7d962abd
commit 0812242f60
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
19 changed files with 2421 additions and 67 deletions

View File

@ -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"

View File

@ -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

View File

@ -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)
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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(())
}
}

View File

@ -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),
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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!();
}
}

View File

@ -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 {

View File

@ -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."))

View File

@ -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());

View File

@ -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..];

View File

@ -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 = "*" }

View File

@ -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(())
}

View File

@ -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(

View File

@ -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());

View File

@ -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 {