imap: add watch

embed
Manos Pitsidianakis 2019-09-06 12:54:12 +03:00
parent c3e3c98fb0
commit 335a1011de
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
5 changed files with 531 additions and 301 deletions

View File

@ -28,6 +28,8 @@ mod operations;
pub use operations::*;
mod connection;
pub use connection::*;
mod watch;
pub use watch::*;
extern crate native_tls;
@ -38,6 +40,7 @@ use crate::backends::RefreshEvent;
use crate::backends::RefreshEventKind::{self, *};
use crate::backends::{BackendFolder, Folder, FolderOperation, MailBackend, RefreshEventConsumer};
use crate::conf::AccountSettings;
use crate::email::parser::BytesExt;
use crate::email::*;
use crate::error::{MeliError, Result};
use fnv::{FnvHashMap, FnvHashSet};
@ -82,22 +85,21 @@ impl MailBackend for ImapType {
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 folder_exists = self.folders[&folder_hash].exists.clone();
let connection = self.connection.clone();
let closure = move || {
let connection = connection.clone();
let tx = tx.clone();
let mut response = String::with_capacity(8 * 1024);
{
let conn = connection.lock();
exit_on_error!(&tx, conn);
let mut conn = conn.unwrap();
let conn = connection.lock();
exit_on_error!(&tx, conn);
let mut conn = conn.unwrap();
debug!("locked for get {}", folder_path);
debug!("locked for get {}", folder_path);
exit_on_error!(&tx,
conn.send_command(format!("EXAMINE {}", folder_path).as_bytes())
conn.read_response(&mut response)
);
}
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);
@ -106,18 +108,17 @@ impl MailBackend for ImapType {
SelectResponse::Ok(ok) => ok.exists,
SelectResponse::Bad(b) => b.exists,
};
{
let mut folder_exists = folder_exists.lock().unwrap();
*folder_exists = exists;
}
while exists > 1 {
let mut envelopes = vec![];
{
let conn = connection.lock();
exit_on_error!(&tx, conn);
let mut conn = conn.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)
);
}
exit_on_error!(&tx,
conn.send_command(format!("UID FETCH {}:{} (FLAGS RFC822.HEADER)", std::cmp::max(exists.saturating_sub(20000), 1), exists).as_bytes())
conn.read_response(&mut response)
);
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
@ -145,10 +146,11 @@ impl MailBackend for ImapType {
tx.send(AsyncStatus::Payload(Err(e)));
}
}
exists = std::cmp::max(exists.saturating_sub(10000), 1);
exists = std::cmp::max(exists.saturating_sub(20000), 1);
debug!("sending payload");
tx.send(AsyncStatus::Payload(Ok(envelopes)));
}
drop(conn);
tx.send(AsyncStatus::Finished);
};
Box::new(closure)
@ -157,286 +159,29 @@ impl MailBackend for ImapType {
}
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));
}
}
})?;
}
let folders = self.imap_folders();
let conn = self.new_connection()?;
let main_conn = self.connection.clone();
let hash_index = self.hash_index.clone();
let uid_index = self.uid_index.clone();
std::thread::Builder::new()
.name(format!("{} imap connection", self.account_name.as_str(),))
.spawn(move || {
let kit = ImapWatchKit {
conn,
main_conn,
hash_index,
uid_index,
folders,
sender,
};
if has_idle {
idle(kit);
} else {
poll_with_examine(kit);
}
})?;
Ok(())
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::backends::{BackendFolder, Folder, FolderHash};
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
pub struct ImapFolder {
@ -27,6 +28,8 @@ pub struct ImapFolder {
pub(super) name: String,
pub(super) parent: Option<FolderHash>,
pub(super) children: Vec<FolderHash>,
pub exists: Arc<Mutex<usize>>,
}
impl BackendFolder for ImapFolder {
@ -57,6 +60,7 @@ impl BackendFolder for ImapFolder {
name: self.name.clone(),
parent: self.parent,
children: self.children.clone(),
exists: self.exists.clone(),
})
}

View File

@ -0,0 +1,479 @@
/*
* 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 super::*;
use std::sync::{Arc, Mutex};
/// Arguments for IMAP watching functions
pub struct ImapWatchKit {
pub conn: ImapConnection,
pub main_conn: Arc<Mutex<ImapConnection>>,
pub hash_index: Arc<Mutex<FnvHashMap<EnvelopeHash, (UID, FolderHash)>>>,
pub uid_index: Arc<Mutex<FnvHashMap<usize, EnvelopeHash>>>,
pub folders: FnvHashMap<FolderHash, ImapFolder>,
pub sender: RefreshEventConsumer,
}
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);
})+
};
}
pub fn poll_with_examine(kit: ImapWatchKit) {
debug!("poll with examine");
let ImapWatchKit {
mut conn,
main_conn,
hash_index,
uid_index,
folders,
sender,
} = kit;
let mut response = String::with_capacity(8 * 1024);
loop {
std::thread::sleep(std::time::Duration::from_millis(5 * 60 * 1000));
for (hash, folder) in &folders {
examine_updates(folder, &sender, &mut conn, &hash_index, &uid_index);
}
let mut main_conn = main_conn.lock().unwrap();
main_conn.send_command(b"NOOP").unwrap();
main_conn.read_response(&mut response).unwrap();
}
}
pub fn idle(kit: ImapWatchKit) {
debug!("IDLE");
/* IDLE only watches the connection's selected mailbox. We will IDLE on INBOX and every ~5
* minutes wake up and poll the others */
let ImapWatchKit {
mut conn,
main_conn,
hash_index,
uid_index,
folders,
sender,
} = kit;
let folder: &ImapFolder = folders
.values()
.find(|f| f.parent.is_none() && f.path().eq_ignore_ascii_case("INBOX"))
.unwrap();
let folder_hash = folder.hash();
let mut response = String::with_capacity(8 * 1024);
exit_on_error!(
sender,
folder_hash,
conn.read_response(&mut response)
conn.send_command(format!("SELECT {}", folder.path()).as_bytes())
conn.read_response(&mut response)
);
debug!("select response {}", &response);
{
let mut prev_exists = folder.exists.lock().unwrap();
*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");
}
};
}
exit_on_error!(sender, folder_hash, conn.send_command(b"IDLE"));
let mut iter = ImapBlockingConnection::from(conn);
let mut beat = std::time::Instant::now();
let mut watch = std::time::Instant::now();
/* duration interval to send heartbeat */
let _26_mins = std::time::Duration::from_secs(26 * 60);
/* duration interval to check other folders for changes */
let _5_mins = std::time::Duration::from_secs(5 * 60);
loop {
while let Some(line) = iter.next() {
let now = std::time::Instant::now();
if now.duration_since(beat) >= _26_mins {
exit_on_error!(
sender,
folder_hash,
iter.conn.set_nonblocking(true)
iter.conn.send_raw(b"DONE")
iter.conn.read_response(&mut response)
iter.conn.send_command(b"IDLE")
iter.conn.set_nonblocking(false)
main_conn.lock().unwrap().send_command(b"NOOP")
main_conn.lock().unwrap().read_response(&mut response)
);
beat = now;
}
if now.duration_since(watch) >= _5_mins {
/* Time to poll the other inboxes */
exit_on_error!(
sender,
folder_hash,
iter.conn.set_nonblocking(true)
iter.conn.send_raw(b"DONE")
iter.conn.read_response(&mut response)
);
for (hash, folder) in &folders {
if *hash == folder_hash {
/* Skip INBOX */
continue;
}
examine_updates(folder, &sender, &mut iter.conn, &hash_index, &uid_index);
}
exit_on_error!(
sender,
folder_hash,
iter.conn.send_command(b"IDLE")
iter.conn.set_nonblocking(false)
main_conn.lock().unwrap().send_command(b"NOOP")
main_conn.lock().unwrap().read_response(&mut response)
);
watch = 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,
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,
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(),
folder.path(),
);
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,
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,
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
* */
let mut prev_exists = folder.exists.lock().unwrap();
debug!("exists {}", n);
if n > *prev_exists {
exit_on_error!(
sender,
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 uid_index.lock().unwrap().contains_key(&uid) {
continue;
}
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(),
folder.path(),
);
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,
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")),
});
}
fn examine_updates(
folder: &ImapFolder,
sender: &RefreshEventConsumer,
conn: &mut ImapConnection,
hash_index: &Arc<Mutex<FnvHashMap<EnvelopeHash, (UID, FolderHash)>>>,
uid_index: &Arc<Mutex<FnvHashMap<usize, EnvelopeHash>>>,
) {
let folder_hash = folder.hash();
debug!("examining folder {} {}", folder_hash, folder.path());
let mut response = String::with_capacity(8 * 1024);
exit_on_error!(
sender,
folder_hash,
conn.send_command(format!("EXAMINE {}", folder.path()).as_bytes())
conn.read_response(&mut response)
);
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);
let mut prev_exists = folder.exists.lock().unwrap();
let n = ok.exists;
if ok.recent > 0 {
{
/* UID SEARCH RECENT */
exit_on_error!(
sender,
folder_hash,
conn.send_command(b"UID SEARCH RECENT")
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,
folder_hash,
conn.send_command(
&[b"UID FETCH", v, b"(FLAGS RFC822.HEADER)"]
.join(&b' '),
)
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(),
folder.path(),
);
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
);
}
}
}
} else if n > *prev_exists {
/* UID FETCH ALL UID, cross-ref, then FETCH difference headers
* */
debug!("exists {}", n);
exit_on_error!(
sender,
folder_hash,
conn.send_command(
&[
b"FETCH",
format!("{}:{}", *prev_exists + 1, n).as_bytes(),
b"(UID FLAGS RFC822.HEADER)",
]
.join(&b' '),
)
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(),
folder.path(),
);
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;
}
}
Err(e) => {
debug!("{:?}", e);
panic!("could not select mailbox");
}
};
}

View File

@ -869,6 +869,9 @@ impl fmt::Display for ThreadView {
impl Component for ThreadView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let total_cols = width!(area);
if self.entries.is_empty() {
return;
}
/* If user has selected another mail to view, change to it */
if self.new_expanded_pos != self.expanded_pos {

View File

@ -40,7 +40,6 @@ use crate::types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate
use std::collections::VecDeque;
use std::fs;
use std::io;
use std::mem;
use std::ops::{Index, IndexMut};
use std::result;
use std::sync::Arc;