forked from meli/meli
1
Fork 0

Compare commits

...

15 Commits

Author SHA1 Message Date
Manos Pitsidianakis d1dca4ac27
melib/imap: fix imap cache without sqlite3 not compiling 2023-04-10 15:49:49 +03:00
Manos Pitsidianakis 61a009c01a
Fix two minor lints 2023-04-06 09:56:56 +03:00
Andrei Zisu 1a4384db08
Use BODY instead of RFC822
RFC3501 seems to prefer BODY attributes as the more modern equivalent
to RFC822. For example, this also allows us to use BODY.PEEK. Since the
fetch methods in melib are tightly coupled with the parser, we have to
add this here if we want to use BODY attributes during fetch.
2023-04-06 09:54:52 +03:00
Manos Pitsidianakis bc11705e85
melib: add text/plain or text/html arg for text decoding
In the function that decodes attachments to text, the default was
Text::Plain. Now it's passed via an argument so that Text::Html can also
be used.
2023-04-06 09:54:11 +03:00
Andrei Zisu 797660b9f6
Make ModSequence publicly accessible
This way it can be imported from this namespace in depending code.
2023-04-06 09:54:11 +03:00
Andrei Zisu 7cad1da7b2
Make UIDStore constructor pub
I honestly forget exactly why this change is needed, so I need to
recheck.
2023-04-06 09:54:11 +03:00
Andrei Zisu 5dd3ead89b
Make UID and UID validity 32bits to match the RFC
In the RFC it says they both add up to 64 bits. Previously they were
type aliases for usize,  which on 64 bit platforms will be 64 bits. As a
consequence, adding up these two data types would amount to 128 bits,
not 64 bits.
2023-04-06 09:54:10 +03:00
Andrei Zisu c7208a168c
Add uid_fetch method to connection
Abstracting out this common method, to promote re-use.
2023-04-06 09:54:10 +03:00
Andrei Zisu f74e3c1472
Add connection method for select
As opposed to the old implementation of select_mailbox, this one does
not interact with the store, but returns any response directly.
2023-04-06 09:54:10 +03:00
Andrei Zisu 7c20f7c82a
Add method to check connection capabilities
Before, the check was being done on the capabilities set directly.
2023-04-06 09:54:09 +03:00
Andrei Zisu 775a2b043a
Remove store references in imap connection
The idea here is to move the store in the IMAP backend in the future. As
a first step, we're wrapping around the state part.
2023-04-06 09:54:09 +03:00
Andrei Zisu 22fd89affc
Stub out select and examine
In the future this will be merged better with upstream, but as a
temporary workaround, I'm just commenting out the code to avoid all
those references to uid_store.

The idea is to move the store into the backend struct, and leave the
connection stateless.
2023-04-06 09:54:09 +03:00
Andrei Zisu b8f4e1e6a8
melib/imap: Allow XOAUTH2 string passed as string
For cases in which the user of melib already knows the token and auth
string and doesn't have to call an outside command.
2023-04-06 09:54:08 +03:00
Andrei Zisu ab6aba300b
melib/imap: Base64 when building XOAUTH command
Moving the encoding later will allow us to have more uniform handling
over authentication methods in the future.
2023-04-06 08:48:00 +03:00
Andrei Zisu 3221c9dda5
Remove unecessary mut modifier
This also makes sense semantically since as_bytes shouldn't be
performing any mutations.
2023-04-06 08:46:34 +03:00
23 changed files with 236 additions and 273 deletions

View File

@ -55,8 +55,8 @@ option.
oauth2 --generate_oauth2_string --user=xxx@gmail.com \
--access_token=ya29.AGy[...]ezLg
The output of this mode will be a base64-encoded string. To use it, connect to a
IMAPFE and pass it as the second argument to the AUTHENTICATE command.
To use the output of this mode, connect to a IMAPFE and pass the base64
encoding of this string as the second argument to the AUTHENTICATE command.
a AUTHENTICATE XOAUTH2 a9sha9sfs[...]9dfja929dk==
"""
@ -240,7 +240,7 @@ def RefreshToken(client_id, client_secret, refresh_token):
return json.loads(response)
def GenerateOAuth2String(username, access_token, base64_encode=True):
def GenerateOAuth2String(username, access_token):
"""Generates an IMAP OAuth2 authentication string.
See https://developers.google.com/google-apps/gmail/oauth2_overview
@ -248,14 +248,11 @@ def GenerateOAuth2String(username, access_token, base64_encode=True):
Args:
username: the username (email address) of the account to authenticate
access_token: An OAuth2 access token.
base64_encode: Whether to base64-encode the output.
Returns:
The SASL argument for the OAuth2 mechanism.
"""
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
if base64_encode:
auth_string = base64.b64encode(bytes(auth_string, 'utf-8'))
return auth_string
@ -331,13 +328,11 @@ def main(argv):
elif options.test_imap_authentication:
RequireOptions(options, 'user', 'access_token')
TestImapAuthentication(options.user,
GenerateOAuth2String(options.user, options.access_token,
base64_encode=False))
GenerateOAuth2String(options.user, options.access_token))
elif options.test_smtp_authentication:
RequireOptions(options, 'user', 'access_token')
TestSmtpAuthentication(options.user,
GenerateOAuth2String(options.user, options.access_token,
base64_encode=False))
GenerateOAuth2String(options.user, options.access_token))
else:
options_parser.print_help()
print('Nothing to do, exiting.')

View File

@ -467,7 +467,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
/// let operation = Box::new(FooOp {});
/// ```
pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>>;
fn as_bytes(&self) -> ResultFuture<Vec<u8>>;
fn fetch_flags(&self) -> ResultFuture<Flag>;
}
@ -487,7 +487,7 @@ impl ReadOnlyOp {
}
impl BackendOp for ReadOnlyOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
self.op.as_bytes()
}
fn fetch_flags(&self) -> ResultFuture<Flag> {

View File

@ -32,7 +32,7 @@ pub use connection::*;
mod watch;
pub use watch::*;
mod cache;
use cache::{ImapCacheReset, ModSequence};
pub use cache::{ImapCacheReset, ModSequence};
pub mod managesieve;
mod untagged;
@ -51,6 +51,7 @@ use futures::stream::Stream;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::hash::Hasher;
use std::pin::Pin;
use std::str::FromStr;
@ -58,8 +59,9 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
pub type ImapNum = usize;
pub type UID = ImapNum;
pub type UIDVALIDITY = UID;
// Data type for UID and UIDVALIDITY defined in RFC 3501 2.3.1.1
pub type UID = u32;
pub type UIDVALIDITY = u32;
pub type MessageSequenceNumber = ImapNum;
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
@ -156,7 +158,7 @@ pub struct UIDStore {
}
impl UIDStore {
fn new(
pub fn new(
account_hash: AccountHash,
account_name: Arc<String>,
event_consumer: BackendEventConsumer,
@ -469,12 +471,9 @@ impl MailBackend for ImapType {
ImapProtocol::IMAP {
extension_use: ImapExtensionUse { idle, .. },
} => {
idle && uid_store
.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
let main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
idle && main_conn_lck.has_capability("IDLE".to_string())
}
_ => false,
};
@ -1260,14 +1259,45 @@ impl ImapType {
let server_username = get_conf_val!(s["server_username"])?;
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
if use_oauth2 && !s.extra.contains_key("server_password_command") {
return Err(Error::new(format!(
"({}) `use_oauth2` use requires `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
s.name,
)));
if use_oauth2 {
if !s.extra.contains_key("server_password_command")
&& !s.extra.contains_key("server_password")
{
return Err(Error::new(format!(
"({}) `use_oauth2` use requires either `server_password` set or `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
s.name,
)));
} else if s.extra.contains_key("server_password_command")
&& s.extra.contains_key("server_password")
{
return Err(Error::new(format!(
"Configuration error ({}): both server_password and server_password_command are set, cannot choose",
s.name,
)));
}
}
let server_password = s.server_password()?;
let server_password = if !s.extra.contains_key("server_password_command") {
get_conf_val!(s["server_password"])?.to_string()
} else {
let invocation = get_conf_val!(s["server_password_command"])?;
let output = std::process::Command::new("sh")
.args(&["-c", invocation])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if !output.status.success() {
return Err(Error::new(format!(
"({}) server_password_command `{}` returned {}: {}",
s.name,
get_conf_val!(s["server_password_command"])?,
output.status,
String::from_utf8_lossy(&output.stderr)
)));
}
std::str::from_utf8(&output.stdout)?.trim_end().to_string()
};
let server_port = get_conf_val!(s["server_port"], 143)?;
let use_tls = get_conf_val!(s["use_tls"], true)?;
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], server_port != 993)?;
@ -1521,21 +1551,31 @@ impl ImapType {
get_conf_val!(s["server_username"])?;
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
keys.insert("server_password_command");
if !s.extra.contains_key("server_password_command") {
if use_oauth2 {
if use_oauth2 {
if !s.extra.contains_key("server_password_command")
&& !s.extra.contains_key("server_password")
{
return Err(Error::new(format!(
"({}) `use_oauth2` use requires `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
"({}) `use_oauth2` use requires either `server_password` set or `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
s.name,
)));
}
get_conf_val!(s["server_password"])?;
} else if s.extra.contains_key("server_password") {
return Err(Error::new(format!(
} else if s.extra.contains_key("server_password_command")
&& s.extra.contains_key("server_password")
{
return Err(Error::new(format!(
"Configuration error ({}): both server_password and server_password_command are set, cannot choose",
s.name)));
} else if s.extra.contains_key("server_password") {
return Err(Error::new(format!(
"Configuration error ({}): both server_password and server_password_command are set, cannot choose",
s.name.as_str(),
)));
}
}
let _ = get_conf_val!(s["server_password"]);
let _ = get_conf_val!(s["server_password_command"]);
get_conf_val!(s["server_port"], 143)?;
get_conf_val!(s["server_port"], 143)?;
let use_tls = get_conf_val!(s["use_tls"], true)?;
let use_starttls = get_conf_val!(s["use_starttls"], false)?;
@ -1735,9 +1775,9 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
let mut conn = connection.lock().await;
let mut response = Vec::with_capacity(8 * 1024);
let max_uid_left = max_uid;
let chunk_size = 250;
let chunk_size: UID = 250;
let mut envelopes = Vec::with_capacity(chunk_size);
let mut envelopes = Vec::with_capacity(chunk_size.try_into().unwrap_or_default());
conn.examine_mailbox(mailbox_hash, &mut response, false)
.await?;
if max_uid_left > 0 {

View File

@ -684,20 +684,20 @@ mod default_m {
pub struct DefaultCache;
impl DefaultCache {
pub fn get(_uid_store: Arc<UIDStore>) -> Result<Box<dyn ImapCache>> {
pub fn get(_: Arc<UIDStore>) -> Result<Box<dyn ImapCache>> {
Ok(Box::new(Self))
}
}
impl ImapCacheReset for DefaultCache {
fn reset_db(uid_store: &UIDStore) -> Result<()> {
fn reset_db(_: &UIDStore) -> Result<()> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
}
impl ImapCache for DefaultCache {
fn reset(&mut self) -> Result<()> {
DefaultCache::reset_db(&self.uid_store)
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn mailbox_state(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<()>> {

View File

@ -20,7 +20,7 @@
*/
use super::protocol_parser::{ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse};
use crate::backends::{MailboxHash, RefreshEvent};
use crate::backends::{AccountHash, BackendEventConsumer, MailboxHash, RefreshEvent};
use crate::connections::{lookup_ipv4, timeout, Connection};
use crate::email::parser::BytesExt;
use crate::error::*;
@ -33,9 +33,11 @@ use std::convert::TryFrom;
use std::future::Future;
use std::iter::FromIterator;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use data_encoding::BASE64;
const IMAP_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60 * 28);
use super::protocol_parser;
@ -110,6 +112,12 @@ pub struct ImapConnection {
pub server_conf: ImapServerConf,
pub sync_policy: SyncPolicy,
pub uid_store: Arc<UIDStore>,
pub account_hash: AccountHash,
pub account_name: Arc<String>,
pub capabilities: Arc<Mutex<Capabilities>>,
pub is_online: Arc<Mutex<(SystemTime, Result<()>)>>,
pub event_consumer: BackendEventConsumer,
}
impl ImapStream {
@ -268,7 +276,6 @@ impl ImapStream {
timeout: server_conf.timeout,
};
if let ImapProtocol::ManageSieve = server_conf.protocol {
use data_encoding::BASE64;
ret.read_response(&mut res).await?;
ret.send_command(
format!(
@ -357,7 +364,11 @@ impl ImapStream {
)));
}
ret.send_command(
format!("AUTHENTICATE XOAUTH2 {}", &server_conf.server_password).as_bytes(),
format!(
"AUTHENTICATE XOAUTH2 {}",
BASE64.encode(server_conf.server_password.as_bytes())
)
.as_bytes(),
)
.await?;
}
@ -560,20 +571,24 @@ impl ImapConnection {
SyncPolicy::None
},
uid_store,
account_hash: AccountHash(0),
account_name: Arc::new("".to_string()),
capabilities: Default::default(),
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(Error::new("Account is uninitialised.")),
))),
event_consumer: BackendEventConsumer::new(Arc::new(|_, _| {})),
}
}
pub fn connect<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if let (time, ref mut status @ Ok(())) = *self.uid_store.is_online.lock().unwrap() {
if let (time, ref mut status @ Ok(())) = *self.is_online.lock().unwrap() {
if SystemTime::now().duration_since(time).unwrap_or_default()
>= IMAP_PROTOCOL_TIMEOUT
{
let err = Error::new(format!(
"Connection timed out after {} seconds",
IMAP_PROTOCOL_TIMEOUT.as_secs()
))
.set_kind(ErrorKind::Timeout);
let err = Error::new("Connection timed out").set_kind(ErrorKind::Timeout);
*status = Err(err.clone());
self.stream = Err(err);
}
@ -598,9 +613,9 @@ impl ImapConnection {
}
let new_stream = ImapStream::new_connection(&self.server_conf, &self.uid_store).await;
if let Err(err) = new_stream.as_ref() {
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
self.is_online.lock().unwrap().1 = Err(err.clone());
} else {
*self.uid_store.is_online.lock().unwrap() = (SystemTime::now(), Ok(()));
*self.is_online.lock().unwrap() = (SystemTime::now(), Ok(()));
}
let (capabilities, stream) = new_stream?;
self.stream = Ok(stream);
@ -672,7 +687,7 @@ impl ImapConnection {
}
ImapProtocol::ManageSieve => {}
}
*self.uid_store.capabilities.lock().unwrap() = capabilities;
*self.capabilities.lock().unwrap() = capabilities;
Ok(())
})
}
@ -686,7 +701,7 @@ impl ImapConnection {
let mut response = Vec::new();
ret.clear();
self.stream.as_mut()?.read_response(&mut response).await?;
*self.uid_store.is_online.lock().unwrap() = (SystemTime::now(), Ok(()));
*self.is_online.lock().unwrap() = (SystemTime::now(), Ok(()));
match self.server_conf.protocol {
ImapProtocol::IMAP { .. } => {
@ -715,8 +730,8 @@ impl ImapConnection {
response_code,
String::from_utf8_lossy(&response)
);
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
(self.event_consumer)(
self.account_hash,
crate::backends::BackendEvent::Notice {
description: response_code.to_string(),
content: None,
@ -732,8 +747,8 @@ impl ImapConnection {
response_code,
String::from_utf8_lossy(&response)
);
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
(self.event_consumer)(
self.account_hash,
crate::backends::BackendEvent::Notice {
description: response_code.to_string(),
content: None,
@ -795,7 +810,7 @@ impl ImapConnection {
}
Err(err)
} else {
*self.uid_store.is_online.lock().unwrap() = (SystemTime::now(), Ok(()));
*self.is_online.lock().unwrap() = (SystemTime::now(), Ok(()));
Ok(())
}
}
@ -825,93 +840,52 @@ impl ImapConnection {
}
}
pub fn has_capability(&self, capability: String) -> bool {
self.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(&capability.as_bytes()))
}
pub async fn select(&mut self, imap_path: String) -> Result<SelectResponse> {
self.send_command(format!("SELECT \"{}\"", imap_path).as_bytes())
.await?;
let mut ret = Vec::new();
self.read_response(&mut ret, RequiredResponses::SELECT_REQUIRED)
.await?;
let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| {
format!("Could not parse select response for mailbox {}", imap_path)
})?;
Ok(select_response)
}
pub async fn uid_fetch<'a>(
&mut self,
range: String,
items_to_fetch: String,
response: &'a mut Vec<u8>, //@TODO ideally instead of having to pass response from the outside, it's better if FetchResponse doesn't borrow response so we can return it by from a function
) -> Result<Vec<protocol_parser::FetchResponse<'a>>> {
self.send_command(format!("UID FETCH {} {}", range, items_to_fetch).as_bytes())
.await?;
self.read_response(response, RequiredResponses::FETCH_REQUIRED)
.await?;
let (_, mut envelopes_list, _) = protocol_parser::fetch_responses(response)?;
Ok(envelopes_list)
}
pub async fn select_mailbox(
&mut self,
mailbox_hash: MailboxHash,
ret: &mut Vec<u8>,
force: bool,
) -> Result<Option<SelectResponse>> {
if !force && self.stream.as_ref()?.current_mailbox == MailboxSelection::Select(mailbox_hash)
{
return Ok(None);
}
let (imap_path, no_select, permissions) = {
let m = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
(
m.imap_path().to_string(),
m.no_select,
m.permissions.clone(),
)
};
if no_select {
return Err(Error::new(format!(
"Trying to select a \\NoSelect mailbox: {}",
&imap_path
))
.set_kind(crate::error::ErrorKind::Bug));
}
self.send_command(format!("SELECT \"{}\"", imap_path).as_bytes())
.await?;
self.read_response(ret, RequiredResponses::SELECT_REQUIRED)
.await?;
debug!(
"{} select response {}",
imap_path,
String::from_utf8_lossy(ret)
);
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
format!("Could not parse select response for mailbox {}", imap_path)
})?;
{
if self.uid_store.keep_offline_cache {
#[cfg(not(feature = "sqlite3"))]
let mut cache_handle = super::cache::DefaultCache::get(self.uid_store.clone())?;
#[cfg(feature = "sqlite3")]
let mut cache_handle = super::cache::Sqlite3Cache::get(self.uid_store.clone())?;
if let Err(err) = cache_handle.mailbox_state(mailbox_hash).and_then(|r| {
if r.is_none() {
cache_handle.clear(mailbox_hash, &select_response)
} else {
Ok(())
}
}) {
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
crate::backends::BackendEvent::from(err),
);
}
}
self.uid_store
.mailboxes
.lock()
.await
.entry(mailbox_hash)
.and_modify(|entry| {
*entry.select.write().unwrap() = Some(select_response.clone());
});
}
{
let mut permissions = permissions.lock().unwrap();
permissions.create_messages = !select_response.read_only;
permissions.remove_messages = !select_response.read_only;
permissions.set_flags = !select_response.read_only;
permissions.rename_messages = !select_response.read_only;
permissions.delete_messages = !select_response.read_only;
}
self.stream.as_mut()?.current_mailbox = MailboxSelection::Select(mailbox_hash);
if self
.uid_store
.msn_index
.lock()
.unwrap()
.get(&mailbox_hash)
.map(|i| i.is_empty())
.unwrap_or(true)
{
self.create_uid_msn_cache(mailbox_hash, 1, &select_response)
.await?;
}
Ok(Some(select_response))
Ok(None)
}
pub async fn examine_mailbox(
@ -920,44 +894,7 @@ impl ImapConnection {
ret: &mut Vec<u8>,
force: bool,
) -> Result<Option<SelectResponse>> {
if !force
&& self.stream.as_ref()?.current_mailbox == MailboxSelection::Examine(mailbox_hash)
{
return Ok(None);
}
let (imap_path, no_select) = {
let m = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
(m.imap_path().to_string(), m.no_select)
};
if no_select {
return Err(Error::new(format!(
"Trying to examine a \\NoSelect mailbox: {}",
&imap_path
))
.set_kind(crate::error::ErrorKind::Bug));
}
self.send_command(format!("EXAMINE \"{}\"", &imap_path).as_bytes())
.await?;
self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED)
.await?;
debug!("examine response {}", String::from_utf8_lossy(ret));
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
format!("Could not parse select response for mailbox {}", imap_path)
})?;
self.stream.as_mut()?.current_mailbox = MailboxSelection::Examine(mailbox_hash);
if !self
.uid_store
.msn_index
.lock()
.unwrap()
.get(&mailbox_hash)
.map(|i| i.is_empty())
.unwrap_or(true)
{
self.create_uid_msn_cache(mailbox_hash, 1, &select_response)
.await?;
}
Ok(Some(select_response))
Ok(None)
}
pub async fn unselect(&mut self) -> Result<()> {
@ -965,7 +902,6 @@ impl ImapConnection {
MailboxSelection::Examine(_) | MailboxSelection::Select(_) => {
let mut response = Vec::with_capacity(8 * 1024);
if self
.uid_store
.capabilities
.lock()
.unwrap()
@ -980,13 +916,8 @@ impl ImapConnection {
* functionality (via a SELECT command with a nonexistent mailbox name or
* reselecting the same mailbox with EXAMINE command)[..]
*/
let mut nonexistent = "blurdybloop".to_string();
{
let mailboxes = self.uid_store.mailboxes.lock().await;
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
nonexistent.push('p');
}
}
let nonexistent = "blurdybloop".to_string();
self.send_command(format!("SELECT \"{}\"", nonexistent).as_bytes())
.await?;
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
@ -999,8 +930,8 @@ impl ImapConnection {
}
pub fn add_refresh_event(&mut self, ev: RefreshEvent) {
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
(self.event_consumer)(
self.account_hash,
crate::backends::BackendEvent::Refresh(ev),
);
}
@ -1011,16 +942,6 @@ impl ImapConnection {
low: usize,
_select_response: &SelectResponse,
) -> Result<()> {
debug_assert!(low > 0);
let mut response = Vec::new();
self.send_command(format!("UID SEARCH {}:*", low).as_bytes())
.await?;
self.read_response(&mut response, RequiredResponses::SEARCH)
.await?;
let mut msn_index_lck = self.uid_store.msn_index.lock().unwrap();
let msn_index = msn_index_lck.entry(mailbox_hash).or_default();
let _ = msn_index.drain(low - 1..);
msn_index.extend(protocol_parser::search_results(&response)?.1.into_iter());
Ok(())
}
}

View File

@ -52,7 +52,7 @@ impl ImapOp {
}
impl BackendOp for ImapOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
let connection = self.connection.clone();
let mailbox_hash = self.mailbox_hash;
let uid = self.uid;

View File

@ -624,8 +624,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"RFC822 {") {
i += b"RFC822 ".len();
} else if input[i..].starts_with(b"BODY[] {") {
i += b"BODY[] ".len();
if let Ok((rest, body)) =
length_data::<_, _, (&[u8], nom::error::ErrorKind), _>(delimited(
tag("{"),
@ -1008,20 +1008,20 @@ fn test_imap_fetch_response() {
);
}
pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<ImapNum>> {
pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<UID>> {
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
|input: &'a [u8]| -> IResult<&'a [u8], Vec<UID>> {
let (input, _) = tag("* SEARCH ")(input)?;
let (input, list) = separated_list1(
tag(b" "),
map_res(is_not(" \r\n"), |s: &[u8]| {
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
UID::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)(input)?;
let (input, _) = tag("\r\n")(input)?;
Ok((input, list))
},
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
|input: &'a [u8]| -> IResult<&'a [u8], Vec<UID>> {
let (input, _) = tag("* SEARCH\r\n")(input)?;
Ok((input, vec![]))
},
@ -1759,7 +1759,7 @@ fn ctl(input: &[u8]) -> IResult<&[u8], u8> {
pub fn generate_envelope_hash(mailbox_path: &str, uid: &UID) -> EnvelopeHash {
let mut h = DefaultHasher::new();
h.write_usize(*uid);
h.write_u32(*uid);
h.write(mailbox_path.as_bytes());
EnvelopeHash(h.finish())
}

View File

@ -45,7 +45,7 @@ impl JmapOp {
}
impl BackendOp for JmapOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
{
let byte_lck = self.store.byte_cache.lock().unwrap();
if byte_lck.contains_key(&self.hash) && byte_lck[&self.hash].bytes.is_some() {

View File

@ -31,6 +31,7 @@ use crate::email::Flag;
use crate::error::{Error, Result};
use crate::shellexpand::ShellExpandTrait;
use futures::stream::Stream;
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
@ -44,7 +45,7 @@ pub struct MaildirOp {
hash_index: HashIndexes,
mailbox_hash: MailboxHash,
hash: EnvelopeHash,
slice: Option<Vec<u8>>,
slice: RefCell<Option<Vec<u8>>>,
}
impl Clone for MaildirOp {
@ -53,7 +54,7 @@ impl Clone for MaildirOp {
hash_index: self.hash_index.clone(),
mailbox_hash: self.mailbox_hash,
hash: self.hash,
slice: None,
slice: RefCell::new(None),
}
}
}
@ -64,7 +65,7 @@ impl MaildirOp {
hash_index,
mailbox_hash,
hash,
slice: None,
slice: RefCell::new(None),
}
}
fn path(&self) -> Result<PathBuf> {
@ -91,8 +92,10 @@ impl MaildirOp {
}
impl<'a> BackendOp for MaildirOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
if self.slice.is_none() {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
let ret = if let Some(bytes) = self.slice.borrow().as_ref() {
bytes.clone()
} else {
let file = std::fs::OpenOptions::new()
.read(true)
.write(false)
@ -100,10 +103,10 @@ impl<'a> BackendOp for MaildirOp {
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
self.slice = Some(contents);
}
let ret = Ok(self.slice.as_ref().unwrap().as_slice().to_vec());
Ok(Box::pin(async move { ret }))
*self.slice.borrow_mut() = Some(contents.clone());
contents
};
Ok(Box::pin(async move { Ok(ret) }))
}
fn fetch_flags(&self) -> ResultFuture<Flag> {

View File

@ -291,8 +291,8 @@ impl MboxOp {
}
impl BackendOp for MboxOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
if self.slice.get_mut().is_none() {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
if self.slice.borrow().is_none() {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
@ -301,9 +301,9 @@ impl BackendOp for MboxOp {
let mut buf_reader = BufReader::new(file);
let mut contents = Vec::new();
buf_reader.read_to_end(&mut contents)?;
*self.slice.get_mut() = Some(contents);
*self.slice.borrow_mut() = Some(contents);
}
let ret = Ok(self.slice.get_mut().as_ref().unwrap().as_slice()
let ret = Ok(self.slice.borrow().as_ref().unwrap().as_slice()
[self.offset..self.offset + self.length]
.to_vec());
Ok(Box::pin(async move { ret }))

View File

@ -52,7 +52,7 @@ impl NntpOp {
}
impl BackendOp for NntpOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
let mailbox_hash = self.mailbox_hash;
let uid = self.uid;
let uid_store = self.uid_store.clone();

View File

@ -25,6 +25,7 @@ use crate::error::{Error, Result};
use crate::shellexpand::ShellExpandTrait;
use crate::{backends::*, Collection};
use smallvec::SmallVec;
use std::cell::RefCell;
use std::collections::{hash_map::HashMap, BTreeMap};
use std::ffi::{CStr, CString, OsStr};
use std::io::Read;
@ -768,7 +769,7 @@ impl MailBackend for NotmuchDb {
lib: self.lib.clone(),
hash,
index: self.index.clone(),
bytes: None,
bytes: RefCell::new(None),
}))
}
@ -1029,21 +1030,26 @@ struct NotmuchOp {
hash: EnvelopeHash,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
database: Arc<DbConnection>,
bytes: Option<Vec<u8>>,
bytes: RefCell<Option<Vec<u8>>>,
#[allow(dead_code)]
lib: Arc<libloading::Library>,
}
impl BackendOp for NotmuchOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
let index_lck = self.index.write().unwrap();
let message = Message::find_message(&self.database, &index_lck[&self.hash])?;
let mut f = std::fs::File::open(message.get_filename())?;
let mut response = Vec::new();
f.read_to_end(&mut response)?;
self.bytes = Some(response);
let ret = Ok(self.bytes.as_ref().unwrap().to_vec());
Ok(Box::pin(async move { ret }))
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
let ret = if let Some(bytes) = self.bytes.borrow().as_ref() {
bytes.to_vec()
} else {
let index_lck = self.index.write().unwrap();
let message = Message::find_message(&self.database, &index_lck[&self.hash])?;
let mut f = std::fs::File::open(message.get_filename())?;
let mut response = Vec::new();
f.read_to_end(&mut response)?;
*self.bytes.borrow_mut() = Some(response.clone());
response
};
Ok(Box::pin(async move { Ok(ret) }))
}
fn fetch_flags(&self) -> ResultFuture<Flag> {

View File

@ -28,7 +28,7 @@
* structure. Addresses in `To`, `From` fields etc are parsed into [`Address`](crate::email::Address) types.
*
* ```
* use melib::{Attachment, Envelope};
* use melib::{Attachment, Envelope, email::attachment_types::Text};
*
* let raw_mail = r#"From: "some name" <some@example.com>
* To: "me" <myself@example.com>
@ -80,7 +80,7 @@
* let body = envelope.body_bytes(raw_mail.as_bytes());
* assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
*
* let body_text = body.text();
* let body_text = body.text(Text::Plain);
* assert_eq!(body_text.as_str(), "hello world.");
*
* let subattachments: Vec<Attachment> = body.attachments();

View File

@ -367,7 +367,7 @@ pub struct Attachment {
impl fmt::Debug for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut text = Vec::with_capacity(4096);
self.get_text_recursive(&mut text);
self.get_text_recursive(&Text::Plain, &mut text);
f.debug_struct("Attachment")
.field("content_type", &self.content_type)
.field("content_transfer_encoding", &self.content_transfer_encoding)
@ -561,7 +561,7 @@ impl Attachment {
false
}
fn get_text_recursive(&self, text: &mut Vec<u8>) {
fn get_text_recursive(&self, kind: &Text, text: &mut Vec<u8>) {
match self.content_type {
ContentType::Text { .. } | ContentType::PGPSignature | ContentType::CMSSignature => {
text.extend(self.decode(Default::default()));
@ -577,15 +577,13 @@ impl Attachment {
.find_map(|(k, v)| if k == b"type" { Some(v) } else { None })
.and_then(|t| parts.iter().find(|a| a.content_type == t.as_slice()))
{
main_attachment.get_text_recursive(text);
main_attachment.get_text_recursive(kind, text);
} else {
for a in parts {
if a.content_disposition.kind.is_inline() {
if let ContentType::Text {
kind: Text::Plain, ..
} = a.content_type
if matches!(&a.content_type, ContentType::Text { kind: a_kind, .. } if a_kind == kind)
{
a.get_text_recursive(text);
a.get_text_recursive(kind, text);
break;
}
}
@ -599,11 +597,9 @@ impl Attachment {
} => {
for a in parts {
if a.content_disposition.kind.is_inline() {
if let ContentType::Text {
kind: Text::Plain, ..
} = a.content_type
if matches!(&a.content_type, ContentType::Text { kind: a_kind, .. } if a_kind == kind)
{
a.get_text_recursive(text);
a.get_text_recursive(kind, text);
break;
}
}
@ -614,7 +610,7 @@ impl Attachment {
} => {
for a in parts {
if a.content_disposition.kind.is_inline() {
a.get_text_recursive(text);
a.get_text_recursive(kind, text);
}
}
}
@ -622,9 +618,9 @@ impl Attachment {
}
}
pub fn text(&self) -> String {
pub fn text(&self, kind: Text) -> String {
let mut text = Vec::with_capacity(self.body.length);
self.get_text_recursive(&mut text);
self.get_text_recursive(&kind, &mut text);
String::from_utf8_lossy(text.as_slice()).into()
}

View File

@ -22,7 +22,7 @@
/*! Compose a `Draft`, with MIME and attachment support */
use super::*;
use crate::email::attachment_types::{
Charset, ContentTransferEncoding, ContentType, MultipartType,
Charset, ContentTransferEncoding, ContentType, MultipartType, Text,
};
use crate::email::attachments::AttachmentBuilder;
use crate::shellexpand::ShellExpandTrait;
@ -99,13 +99,13 @@ impl FromStr for Draft {
}
impl Draft {
pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
pub fn edit(envelope: &Envelope, bytes: &[u8], kind: Text) -> Result<Self> {
let mut ret = Draft::default();
for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) {
ret.headers.insert(k.try_into()?, v.into());
}
ret.body = envelope.body_bytes(bytes).text();
ret.body = envelope.body_bytes(bytes).text(kind);
Ok(ret)
}
@ -383,10 +383,9 @@ fn build_multipart(
}
fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
use ContentType::*;
match a.content_type {
ContentType::Text {
kind: crate::email::attachment_types::Text::Plain,
kind: Text::Plain,
charset: Charset::UTF8,
parameters: ref v,
} if v.is_empty() => {
@ -396,13 +395,13 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
ret.push_str("\r\n");
}
}
Text { .. } => {
ContentType::Text { .. } => {
for line in a.build().into_raw().lines() {
ret.push_str(line);
ret.push_str("\r\n");
}
}
Multipart {
ContentType::Multipart {
boundary: _,
kind,
parts,
@ -418,7 +417,7 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
.collect::<Vec<AttachmentBuilder>>(),
);
}
MessageRfc822 => {
ContentType::MessageRfc822 => {
ret.push_str(&format!(
"Content-Type: {}; charset=\"utf-8\"\r\n",
a.content_type
@ -430,7 +429,7 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
ret.push_str("\r\n");
}
}
PGPSignature => {
ContentType::PGPSignature => {
ret.push_str(&format!(
"Content-Type: {}; charset=\"utf-8\"; name=\"signature.asc\"\r\n",
a.content_type

View File

@ -202,7 +202,7 @@ impl Composer {
let mut ret = Composer::with_account(account_hash, context);
let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash);
ret.draft = Draft::edit(&envelope, bytes)?;
ret.draft = Draft::edit(&envelope, bytes, Text::Plain)?;
ret.account_hash = account_hash;
Ok(ret)

View File

@ -608,7 +608,7 @@ pub trait MailListingTrait: ListingTrait {
let futures: Result<Vec<_>> = envs_to_set
.iter()
.map(|&env_hash| account.operation(env_hash).and_then(|mut op| op.as_bytes()))
.map(|&env_hash| account.operation(env_hash).and_then(|op| op.as_bytes()))
.collect::<Result<Vec<_>>>();
let path_ = path.to_path_buf();
let format = (*format).unwrap_or_default();

View File

@ -282,7 +282,7 @@ impl MailView {
{
match account
.operation(self.coordinates.2)
.and_then(|mut op| op.as_bytes())
.and_then(|op| op.as_bytes())
{
Ok(fut) => {
let mut handle = account.job_executor.spawn_specialized(fut);
@ -1435,7 +1435,7 @@ impl Component for MailView {
self.subview = Some(subview);
self.mode = ViewMode::Subview;
} else {
text.push_str(&attachment.text());
text.push_str(&attachment.text(Text::Plain));
let colors = crate::conf::value(context, "mail.view.body");
self.pager =
Pager::from_string(text, Some(context), Some(0), None, colors);
@ -1997,7 +1997,9 @@ impl Component for MailView {
let (sender, mut receiver) = crate::jobs::oneshot::channel();
let operation = context.accounts[&account_hash].operation(env_hash);
let bytes_job = async move {
let _ = sender.send(operation?.as_bytes()?.await);
let bytes_fut = operation?.as_bytes()?;
let bytes = bytes_fut.await;
let _ = sender.send(bytes);
Ok(())
};
let handle = if context.accounts[&account_hash]

View File

@ -143,7 +143,7 @@ impl EnvelopeView {
ViewMode::Raw => String::from_utf8_lossy(body.body()).into_owned(),
ViewMode::Url => {
let mut t = body_text;
for (lidx, l) in finder.links(&body.text()).enumerate() {
for (lidx, l) in finder.links(&body.text(Text::Plain)).enumerate() {
let offset = if lidx < 10 {
lidx * 3
} else if lidx < 100 {
@ -170,7 +170,7 @@ impl EnvelopeView {
ViewMode::Attachment(aidx) => {
let attachments = body.attachments();
let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
ret.push_str(&attachments[aidx].text());
ret.push_str(&attachments[aidx].text(Text::Plain));
ret
}
}
@ -475,7 +475,7 @@ impl Component for EnvelopeView {
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
let url = {
let finder = LinkFinder::new();
let t = self.mail.body().text();
let t = self.mail.body().text(Text::Plain);
let links: Vec<Link> = finder.links(&t).collect();
if let Some(u) = links.get(lidx) {
u.as_str().to_string()

View File

@ -362,6 +362,7 @@ impl FileSettings {
}
#[cfg(test)]
return Ok(FileSettings::default());
#[cfg(not(test))]
return Err(Error::new("No configuration file found."));
}

View File

@ -279,7 +279,7 @@ struct PluginOp {
}
impl BackendOp for PluginOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
fn as_bytes(&self) -> ResultFuture<Vec<u8>> {
let hash = self.hash;
let channel = self.channel.clone();
Ok(Box::pin(async move {

View File

@ -28,7 +28,7 @@ use melib::search::{
};
use melib::{
backends::{MailBackend, ResultFuture},
email::{Envelope, EnvelopeHash},
email::{attachment_types::Text, Envelope, EnvelopeHash},
log,
sqlite3::{self as melib_sqlite3, rusqlite::params, DatabaseDescription},
thread::{SortField, SortOrder},
@ -156,7 +156,7 @@ pub async fn insert(
.as_bytes()?;
let body = match op.await.map(|bytes| envelope.body_bytes(&bytes)) {
Ok(body) => body.text(),
Ok(body) => body.text(Text::Plain),
Err(err) => {
debug!(
"{}",
@ -305,14 +305,14 @@ pub fn index(context: &mut crate::state::Context, account_index: usize) -> Resul
for chunk in env_hashes.chunks(200) {
ctr += chunk.len();
for env_hash in chunk {
let mut op = backend_mutex.read().unwrap().operation(*env_hash)?;
let bytes = op
.as_bytes()?
let op = backend_mutex.read().unwrap().operation(*env_hash)?;
let bytes_fut = op.as_bytes()?;
let bytes = bytes_fut
.await
.chain_err_summary(|| format!("Failed to open envelope {}", env_hash))?;
let envelopes_lck = acc_mutex.read().unwrap();
if let Some(e) = envelopes_lck.get(env_hash) {
let body = e.body_bytes(&bytes).text().replace('\0', "");
let body = e.body_bytes(&bytes).text(Text::Plain).replace('\0', "");
conn.execute("INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
params![account_id, e.hash().to_be_bytes().to_vec(), e.date_as_str(), e.field_from_to_string(), e.field_to_to_string(), e.field_cc_to_string(), e.field_bcc_to_string(), e.subject().into_owned().trim_end_matches('\u{0}'), e.message_id_display().to_string(), e.in_reply_to_display().map(|f| f.to_string()).unwrap_or_default(), e.field_references_to_string(), i64::from(e.flags().bits()), if e.has_attachments() { 1 } else { 0 }, body, e.date().to_be_bytes().to_vec()],

View File

@ -244,7 +244,7 @@ impl Component for EmbedContainer {
match embed_guard.is_active() {
Ok(WaitStatus::Exited(_, exit_code)) => {
drop(embed_guard);
let embed = self.embed.take();
_ = self.embed.take();
if exit_code != 0 {
context.replies.push_back(UIEvent::Notification(
None,