Compare commits

...
Author SHA1 Message Date
Alexander Krotov 44ee749d29 Remove outdated comment 2020-02-09 18:29:54 +03:00
Alexander Krotov 655a53aea4 Improve logs for prefetch checks 2020-02-09 18:17:29 +03:00
Alexander Krotov 5277013537 Check if contact is blocked or accepted before downloading it 2020-02-09 15:34:14 +03:00
Alexander Krotov 63d129d13d Factor from_field_to_contact_id out of dc_receive_imf 2020-02-09 14:59:01 +03:00
Alexander Krotov f83e178687 Prefetch FROM field 2020-02-09 03:30:13 +03:00
Alexander Krotov 4ca75686f9 Prefetch Message-ID header instead of envelope
Envelope has the same Message-ID, but using other fields from the
envelope, such as From field, is error-prone. They may contain values
different from the message body. As we don't parse the envelope later on,
it is better not to fetch it during prefetch too.
2020-02-09 03:30:13 +03:00
Alexander Krotov 93a826d7ef Do not download messages that are not displayed 2020-02-09 03:30:13 +03:00
Alexander Krotov 9dca24b7a3 Update imap-proto to 0.10.2
It adds support for parsing of BODY[HEADER.FIELDS (...)] fetches.
2020-02-09 03:30:13 +03:00
Alexander Krotov af86962292 Remove unwrap() in prefetch_get_message_id 2020-02-09 03:30:13 +03:00
Alexander Krotov a00f2b3bb8 Fix a typo in prefetch_get_message_id 2020-02-09 03:30:13 +03:00
Alexander Krotov 4e529bfe6a Fix a typo (s/ideling/idling/) 2020-02-09 03:30:13 +03:00
4 changed files with 207 additions and 81 deletions
Generated
+3 -3
View File
@@ -99,7 +99,7 @@ dependencies = [
"byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"imap-proto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1326,7 +1326,7 @@ dependencies = [
[[package]] [[package]]
name = "imap-proto" name = "imap-proto"
version = "0.10.0" version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3405,7 +3405,7 @@ dependencies = [
"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
"checksum image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)" = "53cb19c4e35102e5c6fb9ade5e0e236c5588424dc171a849af3141bf0b47768a" "checksum image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)" = "53cb19c4e35102e5c6fb9ade5e0e236c5588424dc171a849af3141bf0b47768a"
"checksum image-meta 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b00861cbbb254a627d8acc0cec786b484297d896ab8f20fdc8e28536a3e918ef" "checksum image-meta 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b00861cbbb254a627d8acc0cec786b484297d896ab8f20fdc8e28536a3e918ef"
"checksum imap-proto 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11ba57a7d18ba7bca5a50d4723c16a3e532d2b3014d6fd3123910c266214d4f2" "checksum imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
"checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2" "checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2"
"checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+56 -41
View File
@@ -102,40 +102,13 @@ pub fn dc_receive_imf(
// or if From: is equal to SELF (in this case, it is any outgoing messages, // or if From: is equal to SELF (in this case, it is any outgoing messages,
// we do not check Return-Path any more as this is unreliable, see // we do not check Return-Path any more as this is unreliable, see
// https://github.com/deltachat/deltachat-core/issues/150) // https://github.com/deltachat/deltachat-core/issues/150)
let mut from_id = 0; let (from_id, from_id_blocked, incoming_origin) =
let mut from_id_blocked = false; if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
let mut incoming = true; from_field_to_contact_id(context, field_from)?
let mut incoming_origin = Origin::Unknown;
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
incoming = false;
from_id = DC_CONTACT_ID_SELF;
incoming_origin = Origin::OutgoingBcc;
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
from_id = from_ids.get_index(0).cloned().unwrap_or_default();
if let Ok(contact) = Contact::load_from_db(context, from_id) {
incoming_origin = contact.origin;
from_id_blocked = contact.blocked;
}
} else { } else {
warn!(context, "mail has an empty From header: {:?}", field_from); (0, false, Origin::Unknown)
// if there is no from given, from_id stays 0 which is just fine. These messages };
// are very rare, however, we have to add them to the database (they go to the let incoming = from_id != DC_CONTACT_ID_SELF;
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
}
}
let mut to_ids = ContactIds::new(); let mut to_ids = ContactIds::new();
for header_def in &[HeaderDef::To, HeaderDef::Cc] { for header_def in &[HeaderDef::To, HeaderDef::Cc] {
@@ -250,6 +223,47 @@ pub fn dc_receive_imf(
Ok(()) Ok(())
} }
/// Converts "From" field to contact id.
///
/// Also returns whether it is blocked or not and its origin.
pub fn from_field_to_contact_id(
context: &Context,
field_from: &str,
) -> Result<(u32, bool, Origin)> {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
Ok((DC_CONTACT_ID_SELF, false, Origin::OutgoingBcc))
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::load_from_db(context, from_id) {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
Ok((from_id, from_id_blocked, incoming_origin))
} else {
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
Ok((0, false, Origin::Unknown))
}
}
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)] #[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
fn add_parts( fn add_parts(
context: &Context, context: &Context,
@@ -300,8 +314,7 @@ fn add_parts(
} else { } else {
MessengerMessage::No MessengerMessage::No
}; };
// incoming non-chat messages may be discarded; // incoming non-chat messages may be discarded
// maybe this can be optimized later, by checking the state before the message body is downloaded
let mut allow_creation = true; let mut allow_creation = true;
let show_emails = let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default(); ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
@@ -309,11 +322,13 @@ fn add_parts(
&& msgrmsg == MessengerMessage::No && msgrmsg == MessengerMessage::No
{ {
// this message is a classic email not a chat-message nor a reply to one // this message is a classic email not a chat-message nor a reply to one
if show_emails == ShowEmails::Off { match show_emails {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH); ShowEmails::Off => {
allow_creation = false *chat_id = ChatId::new(DC_CHAT_ID_TRASH);
} else if show_emails == ShowEmails::AcceptedContacts { allow_creation = false;
allow_creation = false }
ShowEmails::AcceptedContacts => allow_creation = false,
ShowEmails::All => {}
} }
} }
@@ -1510,7 +1525,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
false false
} }
fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool { pub fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
if let Ok(ids) = mailparse::addrparse(mid_list) { if let Ok(ids) = mailparse::addrparse(mid_list) {
for id in ids.iter() { for id in ids.iter() {
if is_msgrmsg_rfc724_mid(context, id) { if is_msgrmsg_rfc724_mid(context, id) {
+147 -36
View File
@@ -5,6 +5,8 @@
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use num_traits::FromPrimitive;
use async_imap::{ use async_imap::{
error::Result as ImapResult, error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute}, types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
@@ -12,10 +14,16 @@ use async_imap::{
use async_std::sync::{Mutex, RwLock}; use async_std::sync::{Mutex, RwLock};
use async_std::task; use async_std::task;
use mailparse::MailHeaderMap;
use crate::config::*;
use crate::constants::*; use crate::constants::*;
use crate::context::Context; use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf; use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::events::Event; use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::imap_client::*; use crate::imap_client::*;
use crate::job::{job_add, Action}; use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam}; use crate::login_param::{CertificateChecks, LoginParam};
@@ -63,6 +71,9 @@ pub enum Error {
#[fail(display = "IMAP select folder error")] #[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error), SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "Mail parse error")]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "No mailbox selected, folder: {:?}", _0)] #[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String), NoMailbox(String),
@@ -94,6 +105,12 @@ impl From<select_folder::Error> for Error {
} }
} }
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult { pub enum ImapActionResult {
Failed, Failed,
@@ -102,7 +119,20 @@ pub enum ImapActionResult {
Success, Success,
} }
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)"; /// Prefetch:
/// - Message-ID to check if we already have the message.
/// - In-Reply-To and References to check if message is a reply to chat message.
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const JUST_UID: &str = "(UID)"; const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])"; const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
const SELECT_ALL: &str = "1:*"; const SELECT_ALL: &str = "1:*";
@@ -555,6 +585,9 @@ impl Imap {
context: &Context, context: &Context,
folder: S, folder: S,
) -> Result<bool> { ) -> Result<bool> {
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
let (uid_validity, last_seen_uid) = let (uid_validity, last_seen_uid) =
self.select_with_uidvalidity(context, folder.as_ref())?; self.select_with_uidvalidity(context, folder.as_ref())?;
@@ -580,8 +613,8 @@ impl Imap {
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default()); list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
for msg in &list { for fetch in &list {
let cur_uid = msg.uid.unwrap_or_default(); let cur_uid = fetch.uid.unwrap_or_default();
if cur_uid <= last_seen_uid { if cur_uid <= last_seen_uid {
// If the mailbox is not empty, results always include // If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past // at least one UID, even if last_seen_uid+1 is past
@@ -598,20 +631,9 @@ impl Imap {
} }
read_cnt += 1; read_cnt += 1;
let message_id = prefetch_get_message_id(msg).unwrap_or_default(); let headers = get_fetch_headers(fetch)?;
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
if !precheck_imf(context, &message_id, folder.as_ref(), cur_uid) { if precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
message_id,
folder.as_ref()
);
read_errors += 1;
}
} else {
// we know the message-id already or don't want the message otherwise. // we know the message-id already or don't want the message otherwise.
info!( info!(
context, context,
@@ -619,6 +641,33 @@ impl Imap {
message_id, message_id,
folder.as_ref(), folder.as_ref(),
); );
} else {
let show = prefetch_should_download(context, &headers, show_emails)
.map_err(|err| {
warn!(context, "prefetch_should_download error: {}", err);
err
})
.unwrap_or(true);
if !show {
info!(
context,
"Ignoring new message {} from \"{}\".",
message_id,
folder.as_ref(),
);
} else {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
message_id,
folder.as_ref()
);
read_errors += 1;
}
}
} }
if read_errors == 0 { if read_errors == 0 {
new_last_seen_uid = cur_uid; new_last_seen_uid = cur_uid;
@@ -940,9 +989,11 @@ impl Imap {
// double-check that we are deleting the correct message-id // double-check that we are deleting the correct message-id
// this comes at the expense of another imap query // this comes at the expense of another imap query
if let Some(ref mut session) = &mut *self.session.lock().await { if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_fetch(set, PREFETCH_FLAGS).await { match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
Ok(msgs) => { Ok(msgs) => {
if msgs.is_empty() { let fetch = if let Some(fetch) = msgs.first() {
fetch
} else {
warn!( warn!(
context, context,
"Cannot delete on IMAP, {}: imap entry gone '{}'", "Cannot delete on IMAP, {}: imap entry gone '{}'",
@@ -950,9 +1001,11 @@ impl Imap {
message_id, message_id,
); );
return ImapActionResult::Failed; return ImapActionResult::Failed;
} };
let remote_message_id =
prefetch_get_message_id(msgs.first().unwrap()).unwrap_or_default(); let remote_message_id = get_fetch_headers(fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.unwrap_or_default();
if remote_message_id != message_id { if remote_message_id != message_id {
warn!( warn!(
@@ -1233,8 +1286,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
} }
} }
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> { fn parse_message_id(value: &str) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value) let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?; .map_err(|err| format_err!("failed to parse message id {:?}", err))?;
@@ -1245,19 +1297,78 @@ fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
bail!("could not parse message_id: {}", value); bail!("could not parse message_id: {}", value);
} }
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> { fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
if prefetch_msg.envelope().is_none() { let header_bytes = match prefetch_msg.header() {
return Err(Error::Other( Some(header_bytes) => header_bytes,
"prefectch: message has no envelope".to_string(), None => return Ok(Vec::new()),
)); };
let (headers, _) = mailparse::parse_headers(header_bytes)?;
Ok(headers)
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_first_value(&HeaderDef::MessageId.get_headername())? {
Ok(parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
/// Checks if fetch result contains a header
fn prefetch_has_header(headers: &[mailparse::MailHeader], headerdef: HeaderDef) -> Result<bool> {
Ok(headers
.get_first_value(&headerdef.get_headername())?
.is_some())
}
fn prefetch_is_reply_to_chat_message(
context: &Context,
headers: &[mailparse::MailHeader],
) -> Result<bool> {
if let Some(value) = headers.get_first_value(&HeaderDef::InReplyTo.get_headername())? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
} }
let message_id = prefetch_msg.envelope().unwrap().message_id; if let Some(value) = headers.get_first_value(&HeaderDef::References.get_headername())? {
if message_id.is_none() { if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Err(Error::Other("prefetch: No message ID found".to_string())); return Ok(true);
}
} }
parse_message_id(&message_id.unwrap()).map_err(Into::into) Ok(false)
}
fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = prefetch_has_header(&headers, HeaderDef::ChatVersion)?;
let is_autocrypt_setup_message =
prefetch_has_header(&headers, HeaderDef::AutocryptSetupMessage)?;
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
let from_field = headers
.get_first_value(&HeaderDef::From_.get_headername())?
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
ShowEmails::AcceptedContacts => {
is_chat_message || is_reply_to_chat_message || accepted_contact
}
ShowEmails::All => true,
};
let show = show && !blocked_contact;
Ok(show)
} }
#[cfg(test)] #[cfg(test)]
@@ -1267,11 +1378,11 @@ mod tests {
#[test] #[test]
fn test_parse_message_id() { fn test_parse_message_id() {
assert_eq!( assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(), parse_message_id("Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org" "Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
); );
assert_eq!( assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(), parse_message_id("<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org" "Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
); );
} }
+1 -1
View File
@@ -143,7 +143,7 @@ impl JobThread {
if state.jobs_needed { if state.jobs_needed {
info!( info!(
context, context,
"{}-IDLE will not be started as it was interrupted while not ideling.", "{}-IDLE will not be started as it was interrupted while not idling.",
self.name, self.name,
); );
state.jobs_needed = false; state.jobs_needed = false;