mirror of
https://github.com/chatmail/core.git
synced 2026-04-03 14:02:10 +03:00
Compare commits
7 Commits
imap_error
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d03e54d9 | ||
|
|
718f4507b1 | ||
|
|
10925bd5e1 | ||
|
|
f0ad11a8f9 | ||
|
|
6995554f0a | ||
|
|
6fe31278b0 | ||
|
|
34be965525 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,5 +23,3 @@ python/liveconfig*
|
||||
# ignore doxgen generated files
|
||||
deltachat-ffi/html
|
||||
deltachat-ffi/xml
|
||||
|
||||
.rsynclist
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -90,7 +90,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/async-email/async-imap#377d40837028b454c6365ff13e3f35cc341a908e"
|
||||
source = "git+https://github.com/async-email/async-imap#1327f678cf5515842fc309636459372e6ab40db2"
|
||||
dependencies = [
|
||||
"async-attributes 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"async-std 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
||||
@@ -4387,16 +4387,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
|
||||
|
||||
/**
|
||||
* This event is sent out to the inviter when a joiner successfully joined a group.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) contact_id
|
||||
* @return 0
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_MEMBER_ADDED 2062
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -175,15 +175,6 @@ impl ContextWrapper {
|
||||
contact_id as uintptr_t,
|
||||
progress as uintptr_t,
|
||||
),
|
||||
Event::SecurejoinMemberAdded {
|
||||
chat_id,
|
||||
contact_id,
|
||||
} => ffi_cb(
|
||||
self,
|
||||
event_id,
|
||||
chat_id as uintptr_t,
|
||||
contact_id as uintptr_t,
|
||||
),
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
|
||||
@@ -132,8 +132,10 @@ fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int {
|
||||
real_spec = rs.unwrap();
|
||||
}
|
||||
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
|
||||
if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() {
|
||||
read_cnt += 1
|
||||
if suffix == "eml" {
|
||||
if dc_poke_eml_file(context, &real_spec).is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* import a directory */
|
||||
@@ -586,7 +588,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
let members = chat::get_chat_contacts(context, sel_chat.id);
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
|
||||
} else if sel_chat.get_type() == Chattype::Single && members.len() >= 1 {
|
||||
let contact = Contact::get_by_id(context, members[0])?;
|
||||
contact.get_addr().to_string()
|
||||
} else {
|
||||
@@ -860,7 +862,11 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
"archive" | "unarchive" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
let chat_id = arg1.parse()?;
|
||||
chat::archive(context, chat_id, arg0 == "archive")?;
|
||||
chat::archive(
|
||||
context,
|
||||
chat_id,
|
||||
if arg0 == "archive" { true } else { false },
|
||||
)?;
|
||||
}
|
||||
"delchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||
|
||||
@@ -235,7 +235,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 12] = [
|
||||
const IMEX_COMMANDS: [&'static str; 12] = [
|
||||
"initiate-key-transfer",
|
||||
"get-setupcodebegin",
|
||||
"continue-key-transfer",
|
||||
@@ -250,7 +250,7 @@ const IMEX_COMMANDS: [&str; 12] = [
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 11] = [
|
||||
const DB_COMMANDS: [&'static str; 11] = [
|
||||
"info",
|
||||
"open",
|
||||
"close",
|
||||
@@ -264,7 +264,7 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 24] = [
|
||||
const CHAT_COMMANDS: [&'static str; 24] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -290,7 +290,7 @@ const CHAT_COMMANDS: [&str; 24] = [
|
||||
"unarchive",
|
||||
"delchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
const MESSAGE_COMMANDS: [&'static str; 8] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
@@ -300,7 +300,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
"unstar",
|
||||
"delmsg",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 6] = [
|
||||
const CONTACT_COMMANDS: [&'static str; 6] = [
|
||||
"listcontacts",
|
||||
"listverified",
|
||||
"addcontact",
|
||||
@@ -308,7 +308,7 @@ const CONTACT_COMMANDS: [&str; 6] = [
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 9] = [
|
||||
const MISC_COMMANDS: [&'static str; 9] = [
|
||||
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
||||
];
|
||||
|
||||
@@ -334,8 +334,8 @@ impl Hinter for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
||||
static PROMPT: &str = "> ";
|
||||
static COLORED_PROMPT: &'static str = "\x1b[1;32m> \x1b[0m";
|
||||
static PROMPT: &'static str = "> ";
|
||||
|
||||
impl Highlighter for DcHelper {
|
||||
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
|
||||
|
||||
@@ -113,7 +113,7 @@ fn main() {
|
||||
|
||||
println!("stopping threads");
|
||||
|
||||
*running.write().unwrap() = false;
|
||||
*running.clone().write().unwrap() = false;
|
||||
deltachat::job::interrupt_inbox_idle(&ctx, true);
|
||||
deltachat::job::interrupt_smtp_idle(&ctx);
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ DC_EVENT_IMEX_PROGRESS = 2051
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
||||
DC_EVENT_FILE_COPIED = 2055
|
||||
DC_EVENT_IS_OFFLINE = 2081
|
||||
DC_EVENT_GET_STRING = 2091
|
||||
@@ -151,8 +150,7 @@ DC_STR_MSGLOCATIONENABLED = 64
|
||||
DC_STR_MSGLOCATIONDISABLED = 65
|
||||
DC_STR_LOCATION = 66
|
||||
DC_STR_STICKER = 67
|
||||
DC_STR_DEVICE_MESSAGES = 68
|
||||
DC_STR_COUNT = 68
|
||||
DC_STR_COUNT = 67
|
||||
# end const generated
|
||||
|
||||
|
||||
|
||||
@@ -461,20 +461,6 @@ class TestOnlineAccount:
|
||||
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
ev = ac2._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
def test_move_works_on_self_sent(self, acfactory):
|
||||
ac1 = acfactory.get_online_configuring_account(mvbox=True)
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
chat = self.get_chat(ac1, ac2)
|
||||
chat.send_text("message1")
|
||||
chat.send_text("message2")
|
||||
chat.send_text("message3")
|
||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
def test_forward_messages(self, acfactory):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
chat = self.get_chat(ac1, ac2)
|
||||
@@ -782,7 +768,6 @@ class TestOnlineAccount:
|
||||
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
wait_securejoin_inviter_progress(ac1, 1000)
|
||||
ac1._evlogger.get_matching("DC_EVENT_SECUREJOIN_MEMBER_ADDED")
|
||||
|
||||
def test_qr_verified_group_and_chatting(self, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
@@ -794,7 +779,6 @@ class TestOnlineAccount:
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
assert chat2.id >= 10
|
||||
wait_securejoin_inviter_progress(ac1, 1000)
|
||||
ac1._evlogger.get_matching("DC_EVENT_SECUREJOIN_MEMBER_ADDED")
|
||||
|
||||
lp.sec("ac2: read member added message")
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
|
||||
@@ -53,7 +53,7 @@ deps =
|
||||
sphinx==2.2.0
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
sphinx-build -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
|
||||
[testenv:lintdoc]
|
||||
|
||||
@@ -44,8 +44,8 @@ impl str::FromStr for EncryptPreference {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"mutual" => Ok(EncryptPreference::Mutual),
|
||||
"nopreference" => Ok(EncryptPreference::NoPreference),
|
||||
_ => Err(()),
|
||||
"reset" => Ok(EncryptPreference::Reset),
|
||||
_ => Ok(EncryptPreference::NoPreference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
260
src/blob.rs
260
src/blob.rs
@@ -32,11 +32,11 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [BlobError::CreateFailure] is used when the file could not
|
||||
/// [BlobErrorKind::CreateFailure] is used when the file could not
|
||||
/// be created. You can expect [BlobError.cause] to contain an
|
||||
/// underlying error.
|
||||
///
|
||||
/// [BlobError::WriteFailure] is used when the file could not
|
||||
/// [BlobErrorKind::WriteFailure] is used when the file could not
|
||||
/// be written to. You can expect [BlobError.cause] to contain an
|
||||
/// underlying error.
|
||||
pub fn create(
|
||||
@@ -48,12 +48,7 @@ impl<'a> BlobObject<'a> {
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
||||
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
|
||||
file.write_all(data)
|
||||
.map_err(|err| BlobError::WriteFailure {
|
||||
blobdir: blobdir.to_path_buf(),
|
||||
blobname: name.clone(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
.map_err(|err| BlobError::new_write_failure(blobdir, &name, err))?;
|
||||
let blob = BlobObject {
|
||||
blobdir,
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
@@ -76,25 +71,18 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(file) => return Ok((name, file)),
|
||||
Err(err) => {
|
||||
if attempt == max_attempt {
|
||||
return Err(BlobError::CreateFailure {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
return Err(BlobError::new_create_failure(dir, &name, err));
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is supposed to be unreachable, but the compiler doesn't know.
|
||||
Err(BlobError::CreateFailure {
|
||||
blobdir: dir.to_path_buf(),
|
||||
blobname: name,
|
||||
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})
|
||||
Err(BlobError::new_create_failure(
|
||||
dir,
|
||||
&name,
|
||||
format_err!("Unreachable code - supposedly"),
|
||||
))
|
||||
}
|
||||
|
||||
/// Creates a new blob object with unique name by copying an existing file.
|
||||
@@ -107,35 +95,24 @@ impl<'a> BlobObject<'a> {
|
||||
/// # Errors
|
||||
///
|
||||
/// In addition to the errors in [BlobObject::create] the
|
||||
/// [BlobError::CopyFailure] is used when the data can not be
|
||||
/// [BlobErrorKind::CopyFailure] is used when the data can not be
|
||||
/// copied.
|
||||
pub fn create_and_copy(
|
||||
context: &'a Context,
|
||||
src: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: String::from(""),
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| {
|
||||
BlobError::new_copy_failure(context.get_blobdir(), "", src.as_ref(), err)
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
|
||||
let name_for_err = name.clone();
|
||||
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
|
||||
{
|
||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||
let path = context.get_blobdir().join(&name_for_err);
|
||||
let path = context.get_blobdir().join(&name);
|
||||
fs::remove_file(path).ok();
|
||||
}
|
||||
BlobError::CopyFailure {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
blobname: name_for_err,
|
||||
src: src.as_ref().to_path_buf(),
|
||||
cause: err,
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}
|
||||
BlobError::new_copy_failure(context.get_blobdir(), &name, src.as_ref(), err)
|
||||
})?;
|
||||
let blob = BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -179,10 +156,10 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [BlobError::WrongBlobdir] is used if the path is not in
|
||||
/// [BlobErrorKind::WrongBlobdir] is used if the path is not in
|
||||
/// the blob directory.
|
||||
///
|
||||
/// [BlobError::WrongName] is used if the file name does not
|
||||
/// [BlobErrorKind::WrongName] is used if the file name does not
|
||||
/// remain identical after sanitisation.
|
||||
pub fn from_path(
|
||||
context: &Context,
|
||||
@@ -191,21 +168,13 @@ impl<'a> BlobObject<'a> {
|
||||
let rel_path = path
|
||||
.as_ref()
|
||||
.strip_prefix(context.get_blobdir())
|
||||
.map_err(|_| BlobError::WrongBlobdir {
|
||||
blobdir: context.get_blobdir().to_path_buf(),
|
||||
src: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
.map_err(|_| BlobError::new_wrong_blobdir(context.get_blobdir(), path.as_ref()))?;
|
||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
return Err(BlobError::new_wrong_name(path.as_ref()));
|
||||
}
|
||||
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
|
||||
blobname: path.as_ref().to_path_buf(),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
})?;
|
||||
let name = rel_path
|
||||
.to_str()
|
||||
.ok_or_else(|| BlobError::new_wrong_name(path.as_ref()))?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
}
|
||||
|
||||
@@ -218,7 +187,7 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [BlobError::WrongName] is used if the name is not a valid
|
||||
/// [BlobErrorKind::WrongName] is used if the name is not a valid
|
||||
/// blobname, i.e. if [BlobObject::sanitise_name] does modify the
|
||||
/// provided name.
|
||||
pub fn from_name(
|
||||
@@ -230,10 +199,7 @@ impl<'a> BlobObject<'a> {
|
||||
false => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||
return Err(BlobError::WrongName {
|
||||
blobname: PathBuf::from(name),
|
||||
backtrace: failure::Backtrace::new(),
|
||||
});
|
||||
return Err(BlobError::new_wrong_name(name));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -358,50 +324,171 @@ impl<'a> fmt::Display for BlobObject<'a> {
|
||||
}
|
||||
|
||||
/// Errors for the [BlobObject].
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum BlobError {
|
||||
///
|
||||
/// To keep the return type small and thus the happy path fast this
|
||||
/// stores everything on the heap.
|
||||
#[derive(Debug)]
|
||||
pub struct BlobError {
|
||||
inner: Box<BlobErrorInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BlobErrorInner {
|
||||
kind: BlobErrorKind,
|
||||
data: BlobErrorData,
|
||||
backtrace: failure::Backtrace,
|
||||
}
|
||||
|
||||
/// Error kind for [BlobError].
|
||||
///
|
||||
/// Each error kind has associated data in the [BlobErrorData].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BlobErrorKind {
|
||||
/// Failed to create the blob.
|
||||
CreateFailure,
|
||||
/// Failed to write data to blob.
|
||||
WriteFailure,
|
||||
/// Failed to copy data to blob.
|
||||
CopyFailure,
|
||||
/// Blob is not in the blobdir.
|
||||
WrongBlobdir,
|
||||
/// Blob has a bad name.
|
||||
///
|
||||
/// E.g. the name is not sanitised correctly or contains a
|
||||
/// sub-directory.
|
||||
WrongName,
|
||||
}
|
||||
|
||||
/// Associated data for each [BlobError] error kind.
|
||||
///
|
||||
/// This is not stored directly on the [BlobErrorKind] so that the
|
||||
/// kind can stay trivially Copy and Eq. It is however possible to
|
||||
/// create a [BlobError] with mismatching [BlobErrorKind] and
|
||||
/// [BlobErrorData], don't do that.
|
||||
///
|
||||
/// Any blobname stored here is the bare name, without the `$BLOBDIR`
|
||||
/// prefix. All data is owned so that errors do not need to be tied
|
||||
/// to any lifetimes.
|
||||
#[derive(Debug)]
|
||||
enum BlobErrorData {
|
||||
CreateFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[cause]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
cause: failure::Error,
|
||||
},
|
||||
WriteFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
#[cause]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
cause: failure::Error,
|
||||
},
|
||||
CopyFailure {
|
||||
blobdir: PathBuf,
|
||||
blobname: String,
|
||||
src: PathBuf,
|
||||
#[cause]
|
||||
cause: std::io::Error,
|
||||
backtrace: failure::Backtrace,
|
||||
cause: failure::Error,
|
||||
},
|
||||
WrongBlobdir {
|
||||
blobdir: PathBuf,
|
||||
src: PathBuf,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
WrongName {
|
||||
blobname: PathBuf,
|
||||
backtrace: failure::Backtrace,
|
||||
},
|
||||
}
|
||||
|
||||
// Implementing Display is done by hand because the failure
|
||||
// #[fail(display = "...")] syntax does not allow using
|
||||
// `blobdir.display()`.
|
||||
impl BlobError {
|
||||
pub fn kind(&self) -> BlobErrorKind {
|
||||
self.inner.kind
|
||||
}
|
||||
|
||||
fn new_create_failure(
|
||||
blobdir: impl Into<PathBuf>,
|
||||
blobname: impl Into<String>,
|
||||
cause: impl Into<failure::Error>,
|
||||
) -> BlobError {
|
||||
BlobError {
|
||||
inner: Box::new(BlobErrorInner {
|
||||
kind: BlobErrorKind::CreateFailure,
|
||||
data: BlobErrorData::CreateFailure {
|
||||
blobdir: blobdir.into(),
|
||||
blobname: blobname.into(),
|
||||
cause: cause.into(),
|
||||
},
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_write_failure(
|
||||
blobdir: impl Into<PathBuf>,
|
||||
blobname: impl Into<String>,
|
||||
cause: impl Into<failure::Error>,
|
||||
) -> BlobError {
|
||||
BlobError {
|
||||
inner: Box::new(BlobErrorInner {
|
||||
kind: BlobErrorKind::WriteFailure,
|
||||
data: BlobErrorData::WriteFailure {
|
||||
blobdir: blobdir.into(),
|
||||
blobname: blobname.into(),
|
||||
cause: cause.into(),
|
||||
},
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_copy_failure(
|
||||
blobdir: impl Into<PathBuf>,
|
||||
blobname: impl Into<String>,
|
||||
src: impl Into<PathBuf>,
|
||||
cause: impl Into<failure::Error>,
|
||||
) -> BlobError {
|
||||
BlobError {
|
||||
inner: Box::new(BlobErrorInner {
|
||||
kind: BlobErrorKind::CopyFailure,
|
||||
data: BlobErrorData::CopyFailure {
|
||||
blobdir: blobdir.into(),
|
||||
blobname: blobname.into(),
|
||||
src: src.into(),
|
||||
cause: cause.into(),
|
||||
},
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_wrong_blobdir(blobdir: impl Into<PathBuf>, src: impl Into<PathBuf>) -> BlobError {
|
||||
BlobError {
|
||||
inner: Box::new(BlobErrorInner {
|
||||
kind: BlobErrorKind::WrongBlobdir,
|
||||
data: BlobErrorData::WrongBlobdir {
|
||||
blobdir: blobdir.into(),
|
||||
src: src.into(),
|
||||
},
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_wrong_name(blobname: impl Into<PathBuf>) -> BlobError {
|
||||
BlobError {
|
||||
inner: Box::new(BlobErrorInner {
|
||||
kind: BlobErrorKind::WrongName,
|
||||
data: BlobErrorData::WrongName {
|
||||
blobname: blobname.into(),
|
||||
},
|
||||
backtrace: failure::Backtrace::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BlobError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// Match on the data rather than kind, they are equivalent for
|
||||
// identifying purposes but contain the actual data we need.
|
||||
match &self {
|
||||
BlobError::CreateFailure {
|
||||
match &self.inner.data {
|
||||
BlobErrorData::CreateFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(
|
||||
f,
|
||||
@@ -409,7 +496,7 @@ impl fmt::Display for BlobError {
|
||||
blobname,
|
||||
blobdir.display()
|
||||
),
|
||||
BlobError::WriteFailure {
|
||||
BlobErrorData::WriteFailure {
|
||||
blobdir, blobname, ..
|
||||
} => write!(
|
||||
f,
|
||||
@@ -417,7 +504,7 @@ impl fmt::Display for BlobError {
|
||||
blobname,
|
||||
blobdir.display()
|
||||
),
|
||||
BlobError::CopyFailure {
|
||||
BlobErrorData::CopyFailure {
|
||||
blobdir,
|
||||
blobname,
|
||||
src,
|
||||
@@ -429,19 +516,34 @@ impl fmt::Display for BlobError {
|
||||
blobname,
|
||||
blobdir.display(),
|
||||
),
|
||||
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
|
||||
BlobErrorData::WrongBlobdir { blobdir, src } => write!(
|
||||
f,
|
||||
"File path {} is not in blobdir {}",
|
||||
src.display(),
|
||||
blobdir.display(),
|
||||
),
|
||||
BlobError::WrongName { blobname, .. } => {
|
||||
BlobErrorData::WrongName { blobname } => {
|
||||
write!(f, "Blob has a bad name: {}", blobname.display(),)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl failure::Fail for BlobError {
|
||||
fn cause(&self) -> Option<&dyn failure::Fail> {
|
||||
match &self.inner.data {
|
||||
BlobErrorData::CreateFailure { cause, .. }
|
||||
| BlobErrorData::WriteFailure { cause, .. }
|
||||
| BlobErrorData::CopyFailure { cause, .. } => Some(cause.as_fail()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn backtrace(&self) -> Option<&failure::Backtrace> {
|
||||
Some(&self.inner.backtrace)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
16
src/chat.rs
16
src/chat.rs
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
use itertools::Itertools;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::blob::{BlobError, BlobObject};
|
||||
use crate::blob::{BlobErrorKind, BlobObject};
|
||||
use crate::chatlist::*;
|
||||
use crate::config::*;
|
||||
use crate::constants::*;
|
||||
@@ -637,7 +637,7 @@ pub fn create_or_lookup_by_contact_id(
|
||||
context,
|
||||
&context.sql,
|
||||
format!(
|
||||
"INSERT INTO chats (type, name, param, blocked, grpid, created_timestamp) VALUES({}, '{}', '{}', {}, '{}', {})",
|
||||
"INSERT INTO chats (type, name, param, blocked, grpid) VALUES({}, '{}', '{}', {}, '{}')",
|
||||
100,
|
||||
chat_name,
|
||||
match contact_id {
|
||||
@@ -650,7 +650,6 @@ pub fn create_or_lookup_by_contact_id(
|
||||
},
|
||||
create_blocked as u8,
|
||||
contact.get_addr(),
|
||||
time(),
|
||||
),
|
||||
params![],
|
||||
)?;
|
||||
@@ -1389,7 +1388,7 @@ pub fn create_group_chat(
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
"INSERT INTO chats (type, name, grpid, param) VALUES(?, ?, ?, \'U=1\');",
|
||||
params![
|
||||
if verified != VerifiedStatus::Unverified {
|
||||
Chattype::VerifiedGroup
|
||||
@@ -1397,8 +1396,7 @@ pub fn create_group_chat(
|
||||
Chattype::Group
|
||||
},
|
||||
chat_name.as_ref(),
|
||||
grpid,
|
||||
time(),
|
||||
grpid
|
||||
],
|
||||
)?;
|
||||
|
||||
@@ -1488,7 +1486,7 @@ pub(crate) fn add_contact_to_chat_ex(
|
||||
let self_addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
if addr_cmp(contact.get_addr(), &self_addr) {
|
||||
if contact.get_addr() == &self_addr {
|
||||
// ourself is added using DC_CONTACT_ID_SELF, do not add this address explicitly.
|
||||
// if SELF is not in the group, members cannot be added at all.
|
||||
warn!(
|
||||
@@ -1797,8 +1795,8 @@ pub fn set_chat_profile_image(
|
||||
));
|
||||
} else {
|
||||
let image_blob = BlobObject::from_path(context, Path::new(new_image.as_ref())).or_else(
|
||||
|err| match err {
|
||||
BlobError::WrongBlobdir { .. } => {
|
||||
|err| match err.kind() {
|
||||
BlobErrorKind::WrongBlobdir => {
|
||||
BlobObject::create_and_copy(context, Path::new(new_image.as_ref()))
|
||||
}
|
||||
_ => Err(err),
|
||||
|
||||
183
src/chatlist.rs
183
src/chatlist.rs
@@ -6,7 +6,7 @@ use crate::contact::*;
|
||||
use crate::context::*;
|
||||
use crate::error::Result;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::stock::StockMessage;
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
@@ -119,42 +119,46 @@ impl Chatlist {
|
||||
let mut ids = if let Some(query_contact_id) = query_contact_id {
|
||||
// show chats shared with a given contact
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, query_contact_id as i32],
|
||||
concat!(
|
||||
"SELECT c.id, m.id",
|
||||
" FROM chats c",
|
||||
" LEFT JOIN msgs m",
|
||||
" ON c.id=m.chat_id",
|
||||
" AND m.timestamp=(",
|
||||
" SELECT MAX(timestamp)",
|
||||
" FROM msgs",
|
||||
" WHERE chat_id=c.id",
|
||||
" AND hidden=0)",
|
||||
" WHERE c.id>9",
|
||||
" AND c.blocked=0",
|
||||
" AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
" GROUP BY c.id",
|
||||
" ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;"
|
||||
),
|
||||
params![query_contact_id as i32],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
|
||||
// show archived chats
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft],
|
||||
concat!(
|
||||
"SELECT c.id, m.id",
|
||||
" FROM chats c",
|
||||
" LEFT JOIN msgs m",
|
||||
" ON c.id=m.chat_id",
|
||||
" AND m.timestamp=(",
|
||||
" SELECT MAX(timestamp)",
|
||||
" FROM msgs",
|
||||
" WHERE chat_id=c.id",
|
||||
" AND hidden=0)",
|
||||
" WHERE c.id>9",
|
||||
" AND c.blocked=0",
|
||||
" AND c.archived=1",
|
||||
" GROUP BY c.id",
|
||||
" ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;"
|
||||
),
|
||||
params![],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
@@ -164,42 +168,46 @@ impl Chatlist {
|
||||
|
||||
let str_like_cmd = format!("%{}%", query);
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.name LIKE ?
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft, str_like_cmd],
|
||||
concat!(
|
||||
"SELECT c.id, m.id",
|
||||
" FROM chats c",
|
||||
" LEFT JOIN msgs m",
|
||||
" ON c.id=m.chat_id",
|
||||
" AND m.timestamp=(",
|
||||
" SELECT MAX(timestamp)",
|
||||
" FROM msgs",
|
||||
" WHERE chat_id=c.id",
|
||||
" AND hidden=0)",
|
||||
" WHERE c.id>9",
|
||||
" AND c.blocked=0",
|
||||
" AND c.name LIKE ?",
|
||||
" GROUP BY c.id",
|
||||
" ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;"
|
||||
),
|
||||
params![str_like_cmd],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.timestamp=(
|
||||
SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?))
|
||||
WHERE c.id>9
|
||||
AND c.blocked=0
|
||||
AND c.archived=0
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
params![MessageState::OutDraft],
|
||||
concat!(
|
||||
"SELECT c.id, m.id",
|
||||
" FROM chats c",
|
||||
" LEFT JOIN msgs m",
|
||||
" ON c.id=m.chat_id",
|
||||
" AND m.timestamp=(",
|
||||
" SELECT MAX(timestamp)",
|
||||
" FROM msgs",
|
||||
" WHERE chat_id=c.id",
|
||||
" AND hidden=0)",
|
||||
" WHERE c.id>9",
|
||||
" AND c.blocked=0",
|
||||
" AND c.archived=0",
|
||||
" GROUP BY c.id",
|
||||
" ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;"
|
||||
),
|
||||
params![],
|
||||
process_row,
|
||||
process_rows,
|
||||
)?;
|
||||
@@ -289,7 +297,7 @@ impl Chatlist {
|
||||
let lastmsg_id = self.ids[index].1;
|
||||
let mut lastcontact = None;
|
||||
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
||||
let mut lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
@@ -301,6 +309,16 @@ impl Chatlist {
|
||||
None
|
||||
};
|
||||
|
||||
if let Ok(draft) = get_draft(context, chat.id) {
|
||||
if draft.is_some()
|
||||
&& (lastmsg.is_none()
|
||||
|| draft.as_ref().unwrap().timestamp_sort
|
||||
> lastmsg.as_ref().unwrap().timestamp_sort)
|
||||
{
|
||||
lastmsg = draft;
|
||||
}
|
||||
}
|
||||
|
||||
if chat.id == DC_CHAT_ID_ARCHIVED_LINK {
|
||||
ret.text2 = None;
|
||||
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
|
||||
@@ -344,44 +362,3 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
|
||||
params![],
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[test]
|
||||
fn test_try_load() {
|
||||
let t = dummy_context();
|
||||
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
|
||||
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap();
|
||||
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert_eq!(chats.get_chat_id(0), chat_id3);
|
||||
assert_eq!(chats.get_chat_id(1), chat_id2);
|
||||
assert_eq!(chats.get_chat_id(2), chat_id1);
|
||||
|
||||
// drafts are sorted to the top
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hello".to_string()));
|
||||
set_draft(&t.ctx, chat_id2, Some(&mut msg));
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
|
||||
assert_eq!(chats.get_chat_id(0), chat_id2);
|
||||
|
||||
// check chatlist query and archive functionality
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
chat::archive(&t.ctx, chat_id1, true).ok();
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,9 @@ pub fn dc_is_configured(context: &Context) -> bool {
|
||||
/*******************************************************************************
|
||||
* Configure JOB
|
||||
******************************************************************************/
|
||||
// the other dc_job_do_DC_JOB_*() functions are declared static in the c-file
|
||||
#[allow(non_snake_case, unused_must_use)]
|
||||
pub fn JobConfigureImap(context: &Context) {
|
||||
pub fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &Context) {
|
||||
if !context.sql.is_open() {
|
||||
error!(context, "Cannot configure, database not opened.",);
|
||||
progress!(context, 0);
|
||||
@@ -353,21 +354,20 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
}
|
||||
16 => {
|
||||
progress!(context, 900);
|
||||
let create_mvbox = context.get_config_bool(Config::MvboxWatch)
|
||||
|| context.get_config_bool(Config::MvboxMove);
|
||||
let imap = &context.inbox_thread.read().unwrap().imap;
|
||||
if let Err(err) = imap.ensure_configured_folders(context, create_mvbox) {
|
||||
warn!(context, "configuring folders failed: {:?}", err);
|
||||
false
|
||||
let flags: libc::c_int = if context.get_config_bool(Config::MvboxWatch)
|
||||
|| context.get_config_bool(Config::MvboxMove)
|
||||
{
|
||||
DC_CREATE_MVBOX as i32
|
||||
} else {
|
||||
let res = imap.select_with_uidvalidity(context, "INBOX");
|
||||
if let Err(err) = res {
|
||||
error!(context, "could not read INBOX status: {:?}", err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
0
|
||||
};
|
||||
context
|
||||
.inbox_thread
|
||||
.read()
|
||||
.unwrap()
|
||||
.imap
|
||||
.configure_folders(context, flags);
|
||||
true
|
||||
}
|
||||
17 => {
|
||||
progress!(context, 910);
|
||||
@@ -389,10 +389,11 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
// (~30 seconds on a Moto G4 play) and might looks as if message sending is always that slow.
|
||||
e2ee::ensure_secret_key_exists(context);
|
||||
success = true;
|
||||
info!(context, "key generation completed");
|
||||
info!(context, "Configure completed.");
|
||||
progress!(context, 940);
|
||||
break; // We are done here
|
||||
}
|
||||
|
||||
_ => {
|
||||
error!(context, "Internal error: step counter out of bound",);
|
||||
break;
|
||||
@@ -415,6 +416,24 @@ pub fn JobConfigureImap(context: &Context) {
|
||||
context.smtp.clone().lock().unwrap().disconnect();
|
||||
}
|
||||
|
||||
/*
|
||||
if !success {
|
||||
// disconnect if configure did not succeed
|
||||
if imap_connected_here {
|
||||
// context.inbox.read().unwrap().disconnect(context);
|
||||
}
|
||||
if smtp_connected_here {
|
||||
// context.smtp.clone().lock().unwrap().disconnect();
|
||||
}
|
||||
} else {
|
||||
assert!(imap_connected_here && smtp_connected_here);
|
||||
info!(
|
||||
context,
|
||||
0, "Keeping IMAP/SMTP connections open after successful configuration"
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
// remember the entered parameters on success
|
||||
// and restore to last-entered on failure.
|
||||
// this way, the parameters visible to the ui are always in-sync with the current configuration.
|
||||
@@ -587,7 +606,7 @@ pub fn read_autoconf_file(context: &Context, url: &str) -> Option<String> {
|
||||
mod tests {
|
||||
|
||||
use crate::config::*;
|
||||
use crate::configure::JobConfigureImap;
|
||||
use crate::configure::dc_job_do_DC_JOB_CONFIGURE_IMAP;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[test]
|
||||
@@ -597,6 +616,6 @@ mod tests {
|
||||
.set_config(Config::Addr, Some("probably@unexistant.addr"))
|
||||
.unwrap();
|
||||
t.ctx.set_config(Config::MailPw, Some("123456")).unwrap();
|
||||
JobConfigureImap(&t.ctx);
|
||||
dc_job_do_DC_JOB_CONFIGURE_IMAP(&t.ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,21 @@ lazy_static! {
|
||||
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||||
pub enum MoveState {
|
||||
Undefined = 0,
|
||||
Pending = 1,
|
||||
Stay = 2,
|
||||
Moving = 3,
|
||||
}
|
||||
|
||||
impl Default for MoveState {
|
||||
fn default() -> Self {
|
||||
MoveState::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
// some defaults
|
||||
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
|
||||
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
|
||||
@@ -119,6 +134,8 @@ pub const DC_CONTACT_ID_INFO: u32 = 2;
|
||||
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
|
||||
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
pub const DC_CREATE_MVBOX: usize = 1;
|
||||
|
||||
// Flags for empty server job
|
||||
|
||||
pub const DC_EMPTY_MVBOX: u32 = 0x01;
|
||||
|
||||
@@ -272,7 +272,7 @@ impl Contact {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(addr_normalized, addr_self) {
|
||||
if addr_normalized == addr_self {
|
||||
return DC_CONTACT_ID_SELF;
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ impl Contact {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(addr, addr_self) {
|
||||
if addr == addr_self {
|
||||
return Ok((DC_CONTACT_ID_SELF, sth_modified));
|
||||
}
|
||||
|
||||
@@ -585,12 +585,7 @@ impl Contact {
|
||||
|
||||
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
|
||||
|
||||
if peerstate.is_some()
|
||||
&& peerstate
|
||||
.as_ref()
|
||||
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
|
||||
.is_some()
|
||||
{
|
||||
if peerstate.is_some() && peerstate.as_ref().and_then(|p| p.peek_key(0)).is_some() {
|
||||
let peerstate = peerstate.as_ref().unwrap();
|
||||
let p =
|
||||
context.stock_str(if peerstate.prefer_encrypt == EncryptPreference::Mutual {
|
||||
@@ -610,11 +605,11 @@ impl Contact {
|
||||
.map(|k| k.formatted_fingerprint())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_verified = peerstate
|
||||
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
|
||||
.peek_key(2)
|
||||
.map(|k| k.formatted_fingerprint())
|
||||
.unwrap_or_default();
|
||||
let fingerprint_other_unverified = peerstate
|
||||
.peek_key(PeerstateVerifiedStatus::Unverified)
|
||||
.peek_key(0)
|
||||
.map(|k| k.formatted_fingerprint())
|
||||
.unwrap_or_default();
|
||||
if peerstate.addr.is_some() && &loginparam.addr < peerstate.addr.as_ref().unwrap() {
|
||||
@@ -1026,16 +1021,17 @@ fn cat_fingerprint(
|
||||
}
|
||||
|
||||
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
|
||||
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
|
||||
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
|
||||
let norm1 = addr_normalize(addr1.as_ref());
|
||||
let norm2 = addr_normalize(addr2.as_ref());
|
||||
|
||||
norm1 == norm2
|
||||
}
|
||||
|
||||
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
|
||||
if !addr.as_ref().is_empty() {
|
||||
let normalized_addr = addr_normalize(addr.as_ref());
|
||||
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
|
||||
return addr_cmp(addr, self_addr);
|
||||
return normalized_addr == self_addr;
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -1087,10 +1083,6 @@ mod tests {
|
||||
fn test_normalize_addr() {
|
||||
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
|
||||
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
|
||||
|
||||
// normalisation preserves case to allow user-defined spelling.
|
||||
// however, case is ignored on addr_cmp()
|
||||
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1212,11 +1204,4 @@ mod tests {
|
||||
assert_eq!(contact.get_addr(), ""); // we're not configured
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addr_cmp() {
|
||||
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));
|
||||
assert!(addr_cmp(" aa@aa.ORG ", "AA@AA.ORG"));
|
||||
assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,9 +435,10 @@ impl Context {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_mvbox(folder) {
|
||||
if !self.is_inbox(folder) && !self.is_sentbox(folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(msg) = Message::load_from_db(self, msg_id) {
|
||||
if msg.is_setupmessage() {
|
||||
// do not move setup messages;
|
||||
@@ -445,6 +446,10 @@ impl Context {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_mvbox(folder) {
|
||||
message::update_msg_move_state(self, &msg.rfc724_mid, MoveState::Stay);
|
||||
}
|
||||
|
||||
// 1 = dc message, 2 = reply to dc message
|
||||
if 0 != msg.is_dc_message {
|
||||
job_add(
|
||||
@@ -454,6 +459,7 @@ impl Context {
|
||||
Params::new(),
|
||||
0,
|
||||
);
|
||||
message::update_msg_move_state(self, &msg.rfc724_mid, MoveState::Moving);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,25 +495,12 @@ pub struct BobStatus {
|
||||
pub qr_scan: Option<Lot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PerformJobsNeeded {
|
||||
Not,
|
||||
AtOnce,
|
||||
AvoidDos,
|
||||
}
|
||||
|
||||
impl Default for PerformJobsNeeded {
|
||||
fn default() -> Self {
|
||||
Self::Not
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct SmtpState {
|
||||
pub idle: bool,
|
||||
pub suspended: bool,
|
||||
pub doing_jobs: bool,
|
||||
pub perform_jobs_needed: PerformJobsNeeded,
|
||||
pub perform_jobs_needed: i32,
|
||||
pub probe_network: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -392,8 +392,6 @@ unsafe fn add_parts(
|
||||
MessageState::InFresh
|
||||
};
|
||||
*to_id = DC_CONTACT_ID_SELF;
|
||||
let mut needs_stop_ongoing_process = false;
|
||||
|
||||
// handshake messages must be processed _before_ chats are created
|
||||
// (eg. contacs may be marked as verified)
|
||||
if mime_parser.lookup_field("Secure-Join").is_some() {
|
||||
@@ -401,24 +399,11 @@ unsafe fn add_parts(
|
||||
msgrmsg = 1;
|
||||
*chat_id = 0;
|
||||
allow_creation = 1;
|
||||
match handle_securejoin_handshake(context, mime_parser, *from_id) {
|
||||
Ok(ret) => {
|
||||
if ret.hide_this_msg {
|
||||
*hidden = 1;
|
||||
*needs_delete_job = ret.delete_this_msg;
|
||||
state = MessageState::InSeen;
|
||||
}
|
||||
if let Some(status) = ret.bob_securejoin_success {
|
||||
context.bob.write().unwrap().status = status as i32;
|
||||
}
|
||||
needs_stop_ongoing_process = ret.stop_ongoing_process;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Unexpected messaged passed to Secure-Join handshake protocol: {}", err
|
||||
);
|
||||
}
|
||||
let handshake = handle_securejoin_handshake(context, mime_parser, *from_id);
|
||||
if 0 != handshake & DC_HANDSHAKE_STOP_NORMAL_PROCESSING {
|
||||
*hidden = 1;
|
||||
*needs_delete_job = 0 != handshake & DC_HANDSHAKE_ADD_DELETE_JOB;
|
||||
state = MessageState::InSeen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,13 +501,6 @@ unsafe fn add_parts(
|
||||
{
|
||||
state = MessageState::InNoticed;
|
||||
}
|
||||
|
||||
if needs_stop_ongoing_process {
|
||||
// The Secure-Join protocol finished and the group
|
||||
// creation handling is done. Stopping the ongoing
|
||||
// process will let dc_join_securejoin() return.
|
||||
context.stop_ongoing();
|
||||
}
|
||||
} else {
|
||||
// Outgoing
|
||||
|
||||
@@ -1461,7 +1439,7 @@ fn create_group_record(
|
||||
if sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);",
|
||||
"INSERT INTO chats (type, name, grpid, blocked) VALUES(?, ?, ?, ?);",
|
||||
params![
|
||||
if VerifiedStatus::Unverified != create_verified {
|
||||
Chattype::VerifiedGroup
|
||||
@@ -1471,7 +1449,6 @@ fn create_group_record(
|
||||
grpname.as_ref(),
|
||||
grpid.as_ref(),
|
||||
create_blocked,
|
||||
time(),
|
||||
],
|
||||
)
|
||||
.is_err()
|
||||
@@ -1663,11 +1640,7 @@ fn check_verified_properties(
|
||||
info!(context, "{} has verified {}.", contact.get_addr(), to_addr,);
|
||||
let fp = peerstate.gossip_key_fingerprint.clone();
|
||||
if let Some(fp) = fp {
|
||||
peerstate.set_verified(
|
||||
DC_PS_GOSSIP_KEY,
|
||||
&fp,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
);
|
||||
peerstate.set_verified(0, &fp, 2);
|
||||
peerstate.save_to_db(&context.sql, false)?;
|
||||
is_verified = true;
|
||||
}
|
||||
|
||||
@@ -523,13 +523,9 @@ pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Pa
|
||||
}
|
||||
|
||||
/// Write a the given content to provied file path.
|
||||
pub(crate) fn dc_write_file(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
buf: &[u8],
|
||||
) -> Result<(), std::io::Error> {
|
||||
pub(crate) fn dc_write_file(context: &Context, path: impl AsRef<Path>, buf: &[u8]) -> bool {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
fs::write(&path_abs, buf).map_err(|err| {
|
||||
if let Err(err) = fs::write(&path_abs, buf) {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot write {} bytes to \"{}\": {}",
|
||||
@@ -537,8 +533,10 @@ pub(crate) fn dc_write_file(
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
err
|
||||
})
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dc_read_file<P: AsRef<std::path::Path>>(
|
||||
@@ -1325,7 +1323,7 @@ mod tests {
|
||||
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
|
||||
dc_delete_file(context, "$BLOBDIR/foobar-folder");
|
||||
}
|
||||
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content").is_ok());
|
||||
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content"));
|
||||
assert!(dc_file_exist(context, "$BLOBDIR/foobar",));
|
||||
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx"));
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7);
|
||||
@@ -1357,7 +1355,7 @@ mod tests {
|
||||
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));
|
||||
|
||||
let fn0 = "$BLOBDIR/data.data";
|
||||
assert!(dc_write_file(context, &fn0, b"content").is_ok());
|
||||
assert!(dc_write_file(context, &fn0, b"content"));
|
||||
|
||||
assert!(dc_delete_file(context, &fn0));
|
||||
assert!(!dc_file_exist(context, &fn0));
|
||||
|
||||
@@ -75,7 +75,7 @@ impl EncryptHelper {
|
||||
&mut self,
|
||||
factory: &mut MimeFactory,
|
||||
e2ee_guaranteed: bool,
|
||||
min_verified: PeerstateVerifiedStatus,
|
||||
min_verified: libc::c_int,
|
||||
do_gossip: bool,
|
||||
mut in_out_message: *mut Mailmime,
|
||||
imffields_unprotected: *mut mailimf_fields,
|
||||
@@ -118,10 +118,10 @@ impl EncryptHelper {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(key) = peerstate.peek_key(min_verified) {
|
||||
if let Some(key) = peerstate.peek_key(min_verified as usize) {
|
||||
keyring.add_owned(key.clone());
|
||||
if do_gossip {
|
||||
if let Some(header) = peerstate.render_gossip_header(min_verified) {
|
||||
if let Some(header) = peerstate.render_gossip_header(min_verified as usize) {
|
||||
gossip_headers.push(header.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
18
src/error.rs
18
src/error.rs
@@ -36,6 +36,24 @@ pub enum Error {
|
||||
InvalidMsgId,
|
||||
#[fail(display = "Watch folder not found {:?}", _0)]
|
||||
WatchFolderNotFound(String),
|
||||
#[fail(display = "Connection Failed params: {}", _0)]
|
||||
ImapConnectionFailed(String),
|
||||
#[fail(display = "Could not get OAUTH token")]
|
||||
ImapOauthError,
|
||||
#[fail(display = "Could not login as {}", _0)]
|
||||
ImapLoginFailed(String),
|
||||
#[fail(display = "Cannot idle")]
|
||||
ImapMissesIdle,
|
||||
#[fail(display = "Imap IDLE protocol failed to init/complete")]
|
||||
ImapIdleProtocolFailed(String),
|
||||
#[fail(display = "Imap IDLE failed to select folder {:?}", _0)]
|
||||
ImapSelectFailed(String),
|
||||
#[fail(display = "Connect without configured params")]
|
||||
ConnectWithoutConfigure,
|
||||
#[fail(display = "imap operation attempted while imap is torn down")]
|
||||
ImapInTeardown,
|
||||
#[fail(display = "No IMAP Connection established")]
|
||||
ImapNoConnection,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -243,11 +243,4 @@ pub enum Event {
|
||||
/// @return 0
|
||||
#[strum(props(id = "2061"))]
|
||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||
|
||||
/// This event is sent out to the inviter when a joiner successfully joined a group.
|
||||
/// @param data1 (int) chat_id
|
||||
/// @param data2 (int) contact_id
|
||||
/// @return 0
|
||||
#[strum(props(id = "2062"))]
|
||||
SecurejoinMemberAdded { chat_id: u32, contact_id: u32 },
|
||||
}
|
||||
|
||||
598
src/imap.rs
598
src/imap.rs
@@ -12,17 +12,18 @@ use async_imap::{
|
||||
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
|
||||
};
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use async_std::sync::{Arc, Mutex, RwLock};
|
||||
use async_std::task;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::events::Event;
|
||||
use crate::imap_client::*;
|
||||
use crate::job::{job_add, Action};
|
||||
use crate::login_param::{CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_server_uid};
|
||||
use crate::message::{self, update_msg_move_state, update_server_uid};
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::param::Params;
|
||||
use crate::stock::StockMessage;
|
||||
@@ -30,77 +31,6 @@ use crate::wrapmime;
|
||||
|
||||
const DC_IMAP_SEEN: usize = 0x0001;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
#[fail(display = "IMAP Could not obtain imap-session object.")]
|
||||
NoSession,
|
||||
|
||||
#[fail(display = "IMAP Connect without configured params")]
|
||||
ConnectWithoutConfigure,
|
||||
|
||||
#[fail(display = "IMAP Connection Failed params: {}", _0)]
|
||||
ConnectionFailed(String),
|
||||
|
||||
#[fail(display = "IMAP No Connection established")]
|
||||
NoConnection,
|
||||
|
||||
#[fail(display = "IMAP Could not get OAUTH token")]
|
||||
OauthError,
|
||||
|
||||
#[fail(display = "IMAP Could not login as {}", _0)]
|
||||
LoginFailed(String),
|
||||
|
||||
#[fail(display = "IMAP Could not fetch {}", _0)]
|
||||
FetchFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
|
||||
IdleProtocolFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP server does not have IDLE capability")]
|
||||
IdleAbilityMissing,
|
||||
|
||||
#[fail(display = "IMAP Connection Lost or no connection established")]
|
||||
ConnectionLost,
|
||||
|
||||
#[fail(display = "IMAP close/expunge failed: {}", _0)]
|
||||
CloseExpungeFailed(#[cause] async_imap::error::Error),
|
||||
|
||||
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
|
||||
BadFolderName(String),
|
||||
|
||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
||||
InTeardown,
|
||||
|
||||
#[fail(display = "IMAP operation attempted while it is torn down")]
|
||||
SqlError(#[cause] rusqlite::Error),
|
||||
|
||||
#[fail(display = "IMAP got error from elsewhere: {:?}", _0)]
|
||||
WrappedError(#[cause] crate::error::Error),
|
||||
|
||||
#[fail(display = "IMAP other error: {:?}", _0)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
fn from(err: rusqlite::Error) -> Error {
|
||||
Error::SqlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::error::Error> for Error {
|
||||
fn from(err: crate::error::Error) -> Error {
|
||||
Error::WrappedError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for crate::error::Error {
|
||||
fn from(err: Error) -> crate::error::Error {
|
||||
crate::error::Error::Message(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImapActionResult {
|
||||
Failed,
|
||||
@@ -110,16 +40,16 @@ pub enum ImapActionResult {
|
||||
}
|
||||
|
||||
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)";
|
||||
const JUST_UID: &str = "(UID)";
|
||||
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
|
||||
const SELECT_ALL: &str = "1:*";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Imap {
|
||||
config: RwLock<ImapConfig>,
|
||||
session: Mutex<Option<Session>>,
|
||||
connected: Mutex<bool>,
|
||||
interrupt: Mutex<Option<stop_token::StopSource>>,
|
||||
config: Arc<RwLock<ImapConfig>>,
|
||||
|
||||
session: Arc<Mutex<Option<Session>>>,
|
||||
connected: Arc<Mutex<bool>>,
|
||||
interrupt: Arc<Mutex<Option<stop_token::StopSource>>>,
|
||||
skip_next_idle_wait: AtomicBool,
|
||||
should_reconnect: AtomicBool,
|
||||
}
|
||||
@@ -188,10 +118,10 @@ impl Default for ImapConfig {
|
||||
impl Imap {
|
||||
pub fn new() -> Self {
|
||||
Imap {
|
||||
session: Mutex::new(None),
|
||||
config: RwLock::new(ImapConfig::default()),
|
||||
interrupt: Mutex::new(None),
|
||||
connected: Mutex::new(false),
|
||||
session: Arc::new(Mutex::new(None)),
|
||||
config: Arc::new(RwLock::new(ImapConfig::default())),
|
||||
interrupt: Arc::new(Mutex::new(None)),
|
||||
connected: Arc::new(Mutex::new(false)),
|
||||
skip_next_idle_wait: AtomicBool::new(false),
|
||||
should_reconnect: AtomicBool::new(false),
|
||||
}
|
||||
@@ -212,7 +142,7 @@ impl Imap {
|
||||
fn setup_handle_if_needed(&self, context: &Context) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if self.config.read().await.imap_server.is_empty() {
|
||||
return Err(Error::InTeardown);
|
||||
return Err(Error::ImapInTeardown);
|
||||
}
|
||||
|
||||
if self.should_reconnect() {
|
||||
@@ -274,7 +204,7 @@ impl Imap {
|
||||
let res = client.authenticate("XOAUTH2", &auth).await;
|
||||
res
|
||||
} else {
|
||||
return Err(Error::OauthError);
|
||||
return Err(Error::ImapOauthError);
|
||||
}
|
||||
} else {
|
||||
let res = client.login(imap_user, imap_pw).await;
|
||||
@@ -294,7 +224,7 @@ impl Imap {
|
||||
};
|
||||
// IMAP connection failures are reported to users
|
||||
emit_event!(context, Event::ErrorNetwork(message));
|
||||
return Err(Error::ConnectionFailed(err.to_string()));
|
||||
return Err(Error::ImapConnectionFailed(err.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -315,7 +245,10 @@ impl Imap {
|
||||
Event::ErrorNetwork(format!("{} ({})", message, err))
|
||||
);
|
||||
self.trigger_reconnect();
|
||||
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
|
||||
Err(Error::ImapLoginFailed(format!(
|
||||
"cannot login as {}",
|
||||
imap_user
|
||||
)))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -367,10 +300,19 @@ impl Imap {
|
||||
// the trailing underscore is correct
|
||||
|
||||
if self.connect(context, ¶m) {
|
||||
self.ensure_configured_folders(context, true)
|
||||
} else {
|
||||
Err(Error::ConnectionFailed(format!("{}", param).to_string()))
|
||||
if context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured")
|
||||
.unwrap_or_default()
|
||||
< 3
|
||||
{
|
||||
self.configure_folders(context, 0x1);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(Error::ImapConnectionFailed(
|
||||
format!("{}", param).to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// tries connecting to imap account using the specific login
|
||||
@@ -459,30 +401,30 @@ impl Imap {
|
||||
task::block_on(async move {
|
||||
if !context.sql.is_open() {
|
||||
// probably shutdown
|
||||
return Err(Error::InTeardown);
|
||||
return Err(Error::ImapInTeardown);
|
||||
}
|
||||
while self
|
||||
.fetch_from_single_folder(context, &watch_folder)
|
||||
.await?
|
||||
{
|
||||
// We fetch until no more new messages are there.
|
||||
// During the fetch commands new messages may arrive. So we fetch until we do not
|
||||
// get any more. If IDLE is called directly after, there is only a small chance that
|
||||
// messages are missed and delayed until the next IDLE call
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// select a folder, possibly update uid_validity and, if needed,
|
||||
/// expunge the folder to remove delete-marked messages.
|
||||
async fn select_folder<S: AsRef<str>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: Option<S>,
|
||||
) -> Result<()> {
|
||||
) -> ImapActionResult {
|
||||
if self.session.lock().await.is_none() {
|
||||
let mut cfg = self.config.write().await;
|
||||
cfg.selected_folder = None;
|
||||
cfg.selected_folder_needs_expunge = false;
|
||||
return Err(Error::NoSession);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
@@ -490,7 +432,7 @@ impl Imap {
|
||||
if let Some(ref folder) = folder {
|
||||
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
|
||||
if folder.as_ref() == selected_folder {
|
||||
return Ok(());
|
||||
return ImapActionResult::AlreadyDone;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,11 +451,12 @@ impl Imap {
|
||||
info!(context, "close/expunge succeeded");
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(Error::CloseExpungeFailed(err));
|
||||
warn!(context, "failed to close session: {:?}", err);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
self.config.write().await.selected_folder_needs_expunge = false;
|
||||
@@ -522,39 +465,31 @@ impl Imap {
|
||||
// select new folder
|
||||
if let Some(ref folder) = folder {
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
let res = session.select(folder).await;
|
||||
|
||||
// https://tools.ietf.org/html/rfc3501#section-6.3.1
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
match session.select(folder).await {
|
||||
Ok(mailbox) => {
|
||||
let mut config = self.config.write().await;
|
||||
config.selected_folder = Some(folder.as_ref().to_string());
|
||||
config.selected_mailbox = Some(mailbox);
|
||||
Ok(())
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => {
|
||||
self.trigger_reconnect();
|
||||
self.config.write().await.selected_folder = None;
|
||||
Err(Error::ConnectionLost)
|
||||
}
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.as_ref().to_string()))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot select folder: {}; {:?}.",
|
||||
folder.as_ref(),
|
||||
err
|
||||
);
|
||||
|
||||
self.config.write().await.selected_folder = None;
|
||||
self.trigger_reconnect();
|
||||
Err(Error::Other(err.to_string()))
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Error::NoSession)
|
||||
unreachable!();
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
ImapActionResult::Success
|
||||
}
|
||||
|
||||
fn get_config_last_seen_uid<S: AsRef<str>>(&self, context: &Context, folder: S) -> (u32, u32) {
|
||||
@@ -579,154 +514,140 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
/// return Result with (uid_validity, last_seen_uid) tuple.
|
||||
pub(crate) fn select_with_uidvalidity(
|
||||
async fn fetch_from_single_folder<S: AsRef<str>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<(u32, u32)> {
|
||||
task::block_on(async move {
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
|
||||
// compare last seen UIDVALIDITY against the current one
|
||||
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
|
||||
|
||||
let config = self.config.read().await;
|
||||
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
|
||||
|
||||
let new_uid_validity = match mailbox.uid_validity {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
let s = format!("No UIDVALIDITY for folder {:?}", folder);
|
||||
return Err(Error::Other(s));
|
||||
}
|
||||
};
|
||||
|
||||
if new_uid_validity == uid_validity {
|
||||
return Ok((uid_validity, last_seen_uid));
|
||||
folder: S,
|
||||
) -> Result<bool> {
|
||||
match self.select_folder(context, Some(&folder)).await {
|
||||
ImapActionResult::Failed | ImapActionResult::RetryLater => {
|
||||
bail!("Cannot select folder {:?} for fetching.", folder.as_ref());
|
||||
}
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => {}
|
||||
}
|
||||
|
||||
// compare last seen UIDVALIDITY against the current one
|
||||
let (mut uid_validity, mut last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
|
||||
|
||||
let config = self.config.read().await;
|
||||
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
|
||||
|
||||
ensure!(
|
||||
mailbox.uid_validity.is_some(),
|
||||
"Cannot get UIDVALIDITY for folder {:?}",
|
||||
folder.as_ref()
|
||||
);
|
||||
|
||||
let new_uid_validity = mailbox.uid_validity.unwrap();
|
||||
if new_uid_validity != uid_validity {
|
||||
// First time this folder is selected or UIDVALIDITY has changed.
|
||||
// Init lastseenuid and save it to config.
|
||||
info!(
|
||||
context,
|
||||
"new_uid_validity={} current local uid_validity={} lastseenuid={}",
|
||||
new_uid_validity,
|
||||
uid_validity,
|
||||
last_seen_uid
|
||||
);
|
||||
|
||||
if mailbox.exists == 0 {
|
||||
info!(context, "Folder \"{}\" is empty.", folder);
|
||||
info!(context, "Folder \"{}\" is empty.", folder.as_ref());
|
||||
|
||||
// set lastseenuid=0 for empty folders.
|
||||
// id we do not do this here, we'll miss the first message
|
||||
// as we will get in here again and fetch from lastseenuid+1 then
|
||||
|
||||
self.set_config_last_seen_uid(context, &folder, new_uid_validity, 0);
|
||||
return Ok((new_uid_validity, 0));
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// uid_validity has changed or is being set the first time.
|
||||
// find the last seen uid within the new uid_validity scope.
|
||||
let new_last_seen_uid = match mailbox.uid_next {
|
||||
Some(uid_next) => {
|
||||
uid_next - 1 // XXX could uid_next be 0?
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"IMAP folder has no uid_next, fall back to fetching"
|
||||
);
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
// note that we use fetch by sequence number
|
||||
// and thus we only need to get exactly the
|
||||
// last-index message.
|
||||
let set = format!("{}", mailbox.exists);
|
||||
match session.fetch(set, JUST_UID).await {
|
||||
Ok(list) => list[0].uid.unwrap_or_default(),
|
||||
Err(err) => {
|
||||
return Err(Error::FetchFailed(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoConnection);
|
||||
let list = if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
// `FETCH <message sequence number> (UID)`
|
||||
let set = format!("{}", mailbox.exists);
|
||||
match session.fetch(set, PREFETCH_FLAGS).await {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
bail!("fetch failed: {}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::ImapNoConnection);
|
||||
};
|
||||
|
||||
self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid);
|
||||
last_seen_uid = list[0].uid.unwrap_or_else(|| 0);
|
||||
|
||||
// if the UIDVALIDITY has _changed_, decrease lastseenuid by one to avoid gaps (well add 1 below
|
||||
if uid_validity > 0 && last_seen_uid > 1 {
|
||||
last_seen_uid -= 1;
|
||||
}
|
||||
|
||||
uid_validity = new_uid_validity;
|
||||
self.set_config_last_seen_uid(context, &folder, uid_validity, last_seen_uid);
|
||||
info!(
|
||||
context,
|
||||
"uid/validity change: new {}/{} current {}/{}",
|
||||
new_last_seen_uid,
|
||||
new_uid_validity,
|
||||
"lastseenuid initialized to {} for {}@{}",
|
||||
last_seen_uid,
|
||||
folder.as_ref(),
|
||||
uid_validity,
|
||||
last_seen_uid
|
||||
);
|
||||
Ok((new_uid_validity, new_last_seen_uid))
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_from_single_folder<S: AsRef<str>>(
|
||||
&self,
|
||||
context: &Context,
|
||||
folder: S,
|
||||
) -> Result<bool> {
|
||||
let (uid_validity, last_seen_uid) =
|
||||
self.select_with_uidvalidity(context, folder.as_ref())?;
|
||||
}
|
||||
|
||||
let mut read_cnt = 0;
|
||||
let mut read_errors = 0;
|
||||
let mut new_last_seen_uid = 0;
|
||||
|
||||
let mut list = if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
let list = if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
// fetch messages with larger UID than the last one seen
|
||||
// (`UID FETCH lastseenuid+1:*)`, see RFC 4549
|
||||
let set = format!("{}:*", last_seen_uid + 1);
|
||||
match session.uid_fetch(set, PREFETCH_FLAGS).await {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
return Err(Error::FetchFailed(err));
|
||||
bail!("uid_fetch failed: {}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoConnection);
|
||||
return Err(Error::ImapNoConnection);
|
||||
};
|
||||
|
||||
// prefetch info from all unfetched mails
|
||||
let mut new_last_seen_uid = last_seen_uid;
|
||||
let mut read_errors = 0;
|
||||
|
||||
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
|
||||
|
||||
// go through all mails in folder (this is typically _fast_ as we already have the whole list)
|
||||
for msg in &list {
|
||||
let cur_uid = msg.uid.unwrap_or_default();
|
||||
if cur_uid <= last_seen_uid {
|
||||
warn!(
|
||||
context,
|
||||
"unexpected uid {}, last seen was {}", cur_uid, last_seen_uid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
read_cnt += 1;
|
||||
let cur_uid = msg.uid.unwrap_or_else(|| 0);
|
||||
if cur_uid > last_seen_uid {
|
||||
read_cnt += 1;
|
||||
|
||||
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
|
||||
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
|
||||
|
||||
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 {
|
||||
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 {
|
||||
// check failed
|
||||
info!(
|
||||
context,
|
||||
"Read error for message {} from \"{}\", trying over later.",
|
||||
"Skipping message {} from \"{}\" by precheck.",
|
||||
message_id,
|
||||
folder.as_ref()
|
||||
folder.as_ref(),
|
||||
);
|
||||
read_errors += 1;
|
||||
}
|
||||
} else {
|
||||
// we know the message-id already or don't want the message otherwise.
|
||||
info!(
|
||||
context,
|
||||
"Skipping message {} from \"{}\" by precheck.",
|
||||
message_id,
|
||||
folder.as_ref(),
|
||||
);
|
||||
}
|
||||
if read_errors == 0 {
|
||||
new_last_seen_uid = cur_uid;
|
||||
if cur_uid > new_last_seen_uid {
|
||||
new_last_seen_uid = cur_uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if new_last_seen_uid > last_seen_uid {
|
||||
if 0 == read_errors && new_last_seen_uid > 0 {
|
||||
// TODO: it might be better to increase the lastseenuid also on partial errors.
|
||||
// however, this requires to sort the list before going through it above.
|
||||
self.set_config_last_seen_uid(context, &folder, uid_validity, new_last_seen_uid);
|
||||
}
|
||||
|
||||
@@ -837,12 +758,18 @@ impl Imap {
|
||||
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
|
||||
task::block_on(async move {
|
||||
if !self.config.read().await.can_idle {
|
||||
return Err(Error::IdleAbilityMissing);
|
||||
return Err(Error::ImapMissesIdle);
|
||||
}
|
||||
|
||||
self.setup_handle_if_needed(context)?;
|
||||
|
||||
self.select_folder(context, watch_folder.clone()).await?;
|
||||
match self.select_folder(context, watch_folder.clone()).await {
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => {}
|
||||
|
||||
ImapActionResult::Failed | ImapActionResult::RetryLater => {
|
||||
return Err(Error::ImapSelectFailed(format!("{:?}", watch_folder)));
|
||||
}
|
||||
}
|
||||
|
||||
let session = self.session.lock().await.take();
|
||||
let timeout = Duration::from_secs(23 * 60);
|
||||
@@ -852,7 +779,7 @@ impl Imap {
|
||||
// typically also need to change the Insecure branch.
|
||||
IdleHandle::Secure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
return Err(Error::ImapIdleProtocolFailed(err.to_string()));
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
@@ -890,13 +817,13 @@ impl Imap {
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
return Err(Error::ImapIdleProtocolFailed(err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
IdleHandle::Insecure(mut handle) => {
|
||||
if let Err(err) = handle.init().await {
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
return Err(Error::ImapIdleProtocolFailed(err.to_string()));
|
||||
}
|
||||
|
||||
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
|
||||
@@ -934,7 +861,7 @@ impl Imap {
|
||||
// means that we waited long (with idle_wait)
|
||||
// but the network went away/changed
|
||||
self.trigger_reconnect();
|
||||
return Err(Error::IdleProtocolFailed(err));
|
||||
return Err(Error::ImapIdleProtocolFailed(err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -960,44 +887,38 @@ impl Imap {
|
||||
let interval = async_std::stream::interval(Duration::from_secs(60));
|
||||
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
|
||||
*self.interrupt.lock().await = Some(interrupt);
|
||||
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
|
||||
// interrupt_idle has happened before we
|
||||
// provided self.interrupt
|
||||
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
|
||||
info!(context, "fake-idle wait was skipped");
|
||||
} else {
|
||||
// loop until we are interrupted or if we fetched something
|
||||
while let Some(_) = interrupt_interval.next().await {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context) {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.read().await.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break;
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_from_single_folder(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_from_single_folder returned {:?}", res);
|
||||
if res {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
// loop until we are interrupted or if we fetched something
|
||||
while let Some(_) = interrupt_interval.next().await {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if let Err(err) = self.connect_configured(context) {
|
||||
warn!(context, "fake_idle: could not connect: {}", err);
|
||||
continue;
|
||||
}
|
||||
if self.config.read().await.can_idle {
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break;
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
// we are connected, let's see if fetching messages results
|
||||
// in anything. If so, we behave as if IDLE had data but
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
match self.fetch_from_single_folder(context, watch_folder).await {
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_from_single_folder returned {:?}", res);
|
||||
if res {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "could not fetch from folder: {}", err);
|
||||
self.trigger_reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1171,22 +1092,14 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
match self.select_folder(context, Some(&folder)).await {
|
||||
Ok(()) => None,
|
||||
Err(Error::ConnectionLost) => {
|
||||
warn!(context, "Lost imap connection");
|
||||
Some(ImapActionResult::RetryLater)
|
||||
}
|
||||
Err(Error::NoSession) => {
|
||||
warn!(context, "no imap session");
|
||||
Some(ImapActionResult::Failed)
|
||||
}
|
||||
Err(Error::BadFolderName(folder_name)) => {
|
||||
warn!(context, "invalid folder name: {:?}", folder_name);
|
||||
Some(ImapActionResult::Failed)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "failed to select folder: {:?}: {:?}", folder, err);
|
||||
Some(ImapActionResult::RetryLater)
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => None,
|
||||
res => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot select folder {} for preparing IMAP operation", folder
|
||||
);
|
||||
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1288,30 +1201,19 @@ impl Imap {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ensure_configured_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> {
|
||||
let folders_configured = context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured");
|
||||
if folders_configured.unwrap_or_default() >= 3 {
|
||||
// the "3" here we increase if we have future updates to
|
||||
// to folder configuration
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub fn configure_folders(&self, context: &Context, flags: libc::c_int) {
|
||||
task::block_on(async move {
|
||||
if !self.is_connected().await {
|
||||
return Err(Error::NoConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
info!(context, "Configuring IMAP-folders.");
|
||||
|
||||
if let Some(ref mut session) = &mut *self.session.lock().await {
|
||||
let folders = match self.list_folders(session, context).await {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
return Err(Error::Other("list_folders failed".to_string()));
|
||||
}
|
||||
};
|
||||
let folders = self
|
||||
.list_folders(session, context)
|
||||
.await
|
||||
.expect("no folders found");
|
||||
|
||||
let sentbox_folder =
|
||||
folders
|
||||
@@ -1330,7 +1232,7 @@ impl Imap {
|
||||
.find(|folder| folder.name() == "DeltaChat" || folder.name() == fallback_folder)
|
||||
.map(|n| n.name().to_string());
|
||||
|
||||
if mvbox_folder.is_none() && create_mvbox {
|
||||
if mvbox_folder.is_none() && 0 != (flags as usize & DC_CREATE_MVBOX) {
|
||||
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
|
||||
|
||||
match session.create("DeltaChat").await {
|
||||
@@ -1342,7 +1244,7 @@ impl Imap {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})",
|
||||
"Cannot create MVBOX-folder, using trying INBOX subfolder. ({})",
|
||||
err
|
||||
);
|
||||
|
||||
@@ -1364,34 +1266,35 @@ impl Imap {
|
||||
// that may be used by other MUAs to list folders.
|
||||
// for the LIST command, the folder is always visible.
|
||||
if let Some(ref mvbox) = mvbox_folder {
|
||||
if let Err(err) = session.subscribe(mvbox).await {
|
||||
warn!(context, "could not subscribe to {:?}: {:?}", mvbox, err);
|
||||
}
|
||||
// TODO: better error handling
|
||||
session.subscribe(mvbox).await.expect("failed to subscribe");
|
||||
}
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "configured_inbox_folder", Some("INBOX"))?;
|
||||
.set_raw_config(context, "configured_inbox_folder", Some("INBOX"))
|
||||
.ok();
|
||||
if let Some(ref mvbox_folder) = mvbox_folder {
|
||||
context.sql.set_raw_config(
|
||||
context,
|
||||
"configured_mvbox_folder",
|
||||
Some(mvbox_folder),
|
||||
)?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(context, "configured_mvbox_folder", Some(mvbox_folder))
|
||||
.ok();
|
||||
}
|
||||
if let Some(ref sentbox_folder) = sentbox_folder {
|
||||
context.sql.set_raw_config(
|
||||
context,
|
||||
"configured_sentbox_folder",
|
||||
Some(sentbox_folder.name()),
|
||||
)?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(
|
||||
context,
|
||||
"configured_sentbox_folder",
|
||||
Some(sentbox_folder.name()),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "folders_configured", 3)?;
|
||||
.set_raw_config_int(context, "folders_configured", 3)
|
||||
.ok();
|
||||
}
|
||||
info!(context, "FINISHED configuring IMAP-folders.");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1422,35 +1325,33 @@ impl Imap {
|
||||
info!(context, "emptying folder {}", folder);
|
||||
|
||||
if folder.is_empty() {
|
||||
error!(context, "cannot perform empty, folder not set");
|
||||
warn!(context, "cannot perform empty, folder not set");
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.select_folder(context, Some(&folder)).await {
|
||||
// we want to report all error to the user
|
||||
// (no retry should be attempted)
|
||||
error!(
|
||||
context,
|
||||
"Could not select {} for expunging: {:?}", folder, err
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if !self
|
||||
.add_flag_finalized_with_set(context, SELECT_ALL, "\\Deleted")
|
||||
.await
|
||||
{
|
||||
error!(context, "Cannot mark messages for deletion {}", folder);
|
||||
return;
|
||||
}
|
||||
|
||||
// we now trigger expunge to actually delete messages
|
||||
self.config.write().await.selected_folder_needs_expunge = true;
|
||||
match self.select_folder::<String>(context, None).await {
|
||||
Ok(()) => {
|
||||
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
|
||||
match self.select_folder(context, Some(&folder)).await {
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => {
|
||||
if !self
|
||||
.add_flag_finalized_with_set(context, SELECT_ALL, "\\Deleted")
|
||||
.await
|
||||
{
|
||||
warn!(context, "Cannot empty folder {}", folder);
|
||||
} else {
|
||||
// we now trigger expunge to actually delete messages
|
||||
self.config.write().await.selected_folder_needs_expunge = true;
|
||||
if self.select_folder::<String>(context, None).await
|
||||
== ImapActionResult::Success
|
||||
{
|
||||
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"could not perform expunge on empty-marked folder {}", folder
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "expunge failed {}: {:?}", folder, err);
|
||||
ImapActionResult::Failed | ImapActionResult::RetryLater => {
|
||||
warn!(context, "could not select folder {}", folder);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1507,7 +1408,6 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
{
|
||||
if old_server_folder.is_empty() && old_server_uid == 0 {
|
||||
info!(context, "[move] detected bbc-self {}", rfc724_mid,);
|
||||
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
|
||||
job_add(
|
||||
context,
|
||||
Action::MarkseenMsgOnImap,
|
||||
@@ -1517,6 +1417,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
);
|
||||
} else if old_server_folder != server_folder {
|
||||
info!(context, "[move] detected moved message {}", rfc724_mid,);
|
||||
update_msg_move_state(context, &rfc724_mid, MoveState::Stay);
|
||||
}
|
||||
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
@@ -1529,16 +1430,11 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
}
|
||||
|
||||
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
|
||||
if prefetch_msg.envelope().is_none() {
|
||||
return Err(Error::Other(
|
||||
"prefectch: message has no envelope".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
ensure!(
|
||||
prefetch_msg.envelope().is_some(),
|
||||
"Fetched message has no envelope"
|
||||
);
|
||||
let message_id = prefetch_msg.envelope().unwrap().message_id;
|
||||
if message_id.is_none() {
|
||||
return Err(Error::Other("prefetch: No message ID found".to_string()));
|
||||
}
|
||||
|
||||
wrapmime::parse_message_id(&message_id.unwrap()).map_err(Into::into)
|
||||
ensure!(message_id.is_some(), "No message ID found");
|
||||
wrapmime::parse_message_id(&message_id.unwrap())
|
||||
}
|
||||
|
||||
@@ -72,12 +72,11 @@ impl Client {
|
||||
pub async fn secure<S: AsRef<str>>(
|
||||
self,
|
||||
domain: S,
|
||||
certificate_checks: CertificateChecks,
|
||||
_certificate_checks: CertificateChecks,
|
||||
) -> ImapResult<Client> {
|
||||
match self {
|
||||
Client::Insecure(client) => {
|
||||
let tls_config = dc_build_tls_config(certificate_checks);
|
||||
let tls: async_tls::TlsConnector = Arc::new(tls_config).into();
|
||||
let tls = async_tls::TlsConnector::new();
|
||||
|
||||
let client_sec = client.secure(domain, &tls).await?;
|
||||
|
||||
|
||||
31
src/imex.rs
31
src/imex.rs
@@ -367,7 +367,7 @@ pub fn normalize_setup_code(s: &str) -> String {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
|
||||
pub fn job_do_DC_JOB_IMEX_IMAP(context: &Context, job: &Job) -> Result<()> {
|
||||
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
|
||||
let what: Option<ImexMode> = job.param.get_int(Param::Cmd).and_then(ImexMode::from_i32);
|
||||
let param = job.param.get(Param::Arg).unwrap_or_default();
|
||||
@@ -476,15 +476,14 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
|
||||
}
|
||||
|
||||
let path_filename = context.get_blobdir().join(file_name);
|
||||
if dc_write_file(context, &path_filename, &file_blob).is_err() {
|
||||
bail!(
|
||||
"Storage full? Cannot write file {} with {} bytes.",
|
||||
path_filename.display(),
|
||||
file_blob.len(),
|
||||
);
|
||||
} else {
|
||||
if dc_write_file(context, &path_filename, &file_blob) {
|
||||
continue;
|
||||
}
|
||||
bail!(
|
||||
"Storage full? Cannot write file {} with {} bytes.",
|
||||
path_filename.display(),
|
||||
file_blob.len(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
@@ -687,14 +686,14 @@ fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
let (id, public_key, private_key, is_default) = key_pair?;
|
||||
let id = Some(id).filter(|_| is_default != 0);
|
||||
if let Some(key) = public_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
|
||||
if !export_key_to_asc_file(context, &dir, id, &key) {
|
||||
export_errors += 1;
|
||||
}
|
||||
} else {
|
||||
export_errors += 1;
|
||||
}
|
||||
if let Some(key) = private_key {
|
||||
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
|
||||
if !export_key_to_asc_file(context, &dir, id, &key) {
|
||||
export_errors += 1;
|
||||
}
|
||||
} else {
|
||||
@@ -718,7 +717,8 @@ fn export_key_to_asc_file(
|
||||
dir: impl AsRef<Path>,
|
||||
id: Option<i64>,
|
||||
key: &Key,
|
||||
) -> std::io::Result<()> {
|
||||
) -> bool {
|
||||
let mut success = false;
|
||||
let file_name = {
|
||||
let kind = if key.is_public() { "public" } else { "private" };
|
||||
let id = id.map_or("default".into(), |i| i.to_string());
|
||||
@@ -728,13 +728,14 @@ fn export_key_to_asc_file(
|
||||
info!(context, "Exporting key {}", file_name.display());
|
||||
dc_delete_file(context, &file_name);
|
||||
|
||||
let res = key.write_asc_to_file(&file_name, context);
|
||||
if res.is_err() {
|
||||
if !key.write_asc_to_file(&file_name, context) {
|
||||
error!(context, "Cannot write key to {}", file_name.display());
|
||||
} else {
|
||||
context.call_cb(Event::ImexFileWritten(file_name));
|
||||
success = true;
|
||||
}
|
||||
res
|
||||
|
||||
success
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -798,7 +799,7 @@ mod tests {
|
||||
let base64 = include_str!("../test-data/key/public.asc");
|
||||
let key = Key::from_base64(base64, KeyType::Public).unwrap();
|
||||
let blobdir = "$BLOBDIR";
|
||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
|
||||
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key));
|
||||
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
|
||||
let filename = format!("{}/public-key-default.asc", blobdir);
|
||||
let bytes = std::fs::read(&filename).unwrap();
|
||||
|
||||
455
src/job.rs
455
src/job.rs
@@ -1,3 +1,4 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
@@ -8,24 +9,21 @@ use crate::chat;
|
||||
use crate::config::Config;
|
||||
use crate::configure::*;
|
||||
use crate::constants::*;
|
||||
use crate::context::{Context, PerformJobsNeeded};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Error;
|
||||
use crate::events::Event;
|
||||
use crate::imap::*;
|
||||
use crate::imex::*;
|
||||
use crate::job_thread::JobThread;
|
||||
use crate::location;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::MsgId;
|
||||
use crate::message::{self, Message, MessageState};
|
||||
use crate::mimefactory::{vec_contains_lowercase, Loaded, MimeFactory};
|
||||
use crate::param::*;
|
||||
use crate::smtp::SmtpError;
|
||||
use crate::sql;
|
||||
|
||||
// results in ~3 weeks for the last backoff timespan
|
||||
const JOB_RETRIES: u32 = 17;
|
||||
|
||||
/// Thread IDs
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(i32)]
|
||||
@@ -35,6 +33,14 @@ enum Thread {
|
||||
Smtp = 5000,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum ThreadExecutor {
|
||||
Inbox,
|
||||
Mvbox,
|
||||
Sentbox,
|
||||
Smtp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
enum TryAgain {
|
||||
Dont,
|
||||
@@ -111,7 +117,7 @@ pub struct Job {
|
||||
pub foreign_id: u32,
|
||||
pub desired_timestamp: i64,
|
||||
pub added_timestamp: i64,
|
||||
pub tries: u32,
|
||||
pub tries: i32,
|
||||
pub param: Params,
|
||||
try_again: TryAgain,
|
||||
pub pending_error: Option<String>,
|
||||
@@ -141,7 +147,7 @@ impl Job {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn SendMsgToSmtp(&mut self, context: &Context) {
|
||||
fn do_DC_JOB_SEND(&mut self, context: &Context) {
|
||||
/* connect to SMTP server, if not yet done */
|
||||
if !context.smtp.lock().unwrap().is_connected() {
|
||||
let loginparam = LoginParam::from_database(context, "configured_");
|
||||
@@ -185,22 +191,11 @@ impl Job {
|
||||
// otherwise might send it twice.
|
||||
let mut smtp = context.smtp.lock().unwrap();
|
||||
match smtp.send(context, recipients_list, body, self.job_id) {
|
||||
Err(SmtpError::SendError(err)) => {
|
||||
// Remote error, retry later.
|
||||
Err(err) => {
|
||||
smtp.disconnect();
|
||||
info!(context, "SMTP failed to send: {}", err);
|
||||
warn!(context, "smtp failed: {}", err);
|
||||
self.try_again_later(TryAgain::AtOnce, Some(err.to_string()));
|
||||
}
|
||||
Err(SmtpError::EnvelopeError(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect();
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
}
|
||||
Err(SmtpError::NoTransport) => {
|
||||
// Should never happen.
|
||||
// It does not even make sense to disconnect here.
|
||||
error!(context, "SMTP job failed because SMTP has no transport");
|
||||
}
|
||||
Ok(()) => {
|
||||
// smtp success, update db ASAP, then delete smtp file
|
||||
if 0 != self.foreign_id {
|
||||
@@ -224,14 +219,18 @@ impl Job {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn MoveMsg(&mut self, context: &Context) {
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
fn do_DC_JOB_MOVE_MSG(&mut self, context: &Context, thread: ThreadExecutor) {
|
||||
let t = get_mailbox_for_executor(context, thread);
|
||||
let imap_inbox = &t.read().unwrap().imap;
|
||||
|
||||
if let Ok(msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
if let Err(err) = imap_inbox.ensure_configured_folders(context, true) {
|
||||
self.try_again_later(TryAgain::StandardDelay, None);
|
||||
warn!(context, "could not configure folders: {:?}", err);
|
||||
return;
|
||||
if context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured")
|
||||
.unwrap_or_default()
|
||||
< 3
|
||||
{
|
||||
imap_inbox.configure_folders(context, 0x1i32);
|
||||
}
|
||||
let dest_folder = context
|
||||
.sql
|
||||
@@ -266,8 +265,9 @@ impl Job {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn DeleteMsgOnImap(&mut self, context: &Context) {
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
fn do_DC_JOB_DELETE_MSG_ON_IMAP(&mut self, context: &Context, thread: ThreadExecutor) {
|
||||
let t = get_mailbox_for_executor(context, thread);
|
||||
let imap_inbox = &t.read().unwrap().imap;
|
||||
|
||||
if let Ok(mut msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
@@ -295,24 +295,26 @@ impl Job {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn EmptyServer(&mut self, context: &Context) {
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
if self.foreign_id & DC_EMPTY_MVBOX > 0 {
|
||||
fn do_DC_JOB_EMPTY_SERVER(&mut self, context: &Context, thread: ThreadExecutor) {
|
||||
let t = get_mailbox_for_executor(context, thread);
|
||||
let imap_inbox = &t.read().unwrap().imap;
|
||||
|
||||
if thread == ThreadExecutor::Mvbox && DC_EMPTY_MVBOX > 0 {
|
||||
if let Some(mvbox_folder) = context
|
||||
.sql
|
||||
.get_raw_config(context, "configured_mvbox_folder")
|
||||
{
|
||||
imap_inbox.empty_folder(context, &mvbox_folder);
|
||||
}
|
||||
}
|
||||
if self.foreign_id & DC_EMPTY_INBOX > 0 {
|
||||
} else if thread == ThreadExecutor::Inbox && DC_EMPTY_INBOX > 0 {
|
||||
imap_inbox.empty_folder(context, "INBOX");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn MarkseenMsgOnImap(&mut self, context: &Context) {
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
fn do_DC_JOB_MARKSEEN_MSG_ON_IMAP(&mut self, context: &Context, thread: ThreadExecutor) {
|
||||
let t = get_mailbox_for_executor(context, thread);
|
||||
let imap_inbox = &t.read().unwrap().imap;
|
||||
|
||||
if let Ok(msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
@@ -339,23 +341,29 @@ impl Job {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn MarkseenMdnOnImap(&mut self, context: &Context) {
|
||||
fn do_DC_JOB_MARKSEEN_MDN_ON_IMAP(&mut self, context: &Context, thread: ThreadExecutor) {
|
||||
let folder = self
|
||||
.param
|
||||
.get(Param::ServerFolder)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uid = self.param.get_int(Param::ServerUid).unwrap_or_default() as u32;
|
||||
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
|
||||
|
||||
let t = get_mailbox_for_executor(context, thread);
|
||||
let imap_inbox = &t.read().unwrap().imap;
|
||||
|
||||
if imap_inbox.set_seen(context, &folder, uid) == ImapActionResult::RetryLater {
|
||||
self.try_again_later(TryAgain::StandardDelay, None);
|
||||
return;
|
||||
}
|
||||
if 0 != self.param.get_int(Param::AlsoMove).unwrap_or_default() {
|
||||
if let Err(err) = imap_inbox.ensure_configured_folders(context, true) {
|
||||
self.try_again_later(TryAgain::StandardDelay, None);
|
||||
warn!(context, "configuring folders failed: {:?}", err);
|
||||
return;
|
||||
if context
|
||||
.sql
|
||||
.get_raw_config_int(context, "folders_configured")
|
||||
.unwrap_or_default()
|
||||
< 3
|
||||
{
|
||||
imap_inbox.configure_folders(context, 0x1i32);
|
||||
}
|
||||
let dest_folder = context
|
||||
.sql
|
||||
@@ -486,7 +494,7 @@ pub fn perform_smtp_jobs(context: &Context) {
|
||||
|
||||
let probe_smtp_network = state.probe_network;
|
||||
state.probe_network = false;
|
||||
state.perform_jobs_needed = PerformJobsNeeded::Not;
|
||||
state.perform_jobs_needed = 0;
|
||||
|
||||
if state.suspended {
|
||||
info!(context, "SMTP-jobs suspended.",);
|
||||
@@ -497,7 +505,7 @@ pub fn perform_smtp_jobs(context: &Context) {
|
||||
};
|
||||
|
||||
info!(context, "SMTP-jobs started...",);
|
||||
job_perform(context, Thread::Smtp, probe_smtp_network);
|
||||
job_perform(context, ThreadExecutor::Smtp, probe_smtp_network);
|
||||
info!(context, "SMTP-jobs ended.");
|
||||
|
||||
{
|
||||
@@ -514,27 +522,24 @@ pub fn perform_smtp_idle(context: &Context) {
|
||||
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
match state.perform_jobs_needed {
|
||||
PerformJobsNeeded::AtOnce => {
|
||||
info!(
|
||||
context,
|
||||
"SMTP-idle will not be started because of waiting jobs.",
|
||||
);
|
||||
}
|
||||
PerformJobsNeeded::Not | PerformJobsNeeded::AvoidDos => {
|
||||
let dur = get_next_wakeup_time(context, Thread::Smtp);
|
||||
if state.perform_jobs_needed == 1 {
|
||||
info!(
|
||||
context,
|
||||
"SMTP-idle will not be started because of waiting jobs.",
|
||||
);
|
||||
} else {
|
||||
let dur = get_next_wakeup_time(context, Thread::Smtp);
|
||||
|
||||
loop {
|
||||
let res = cvar.wait_timeout(state, dur).unwrap();
|
||||
state = res.0;
|
||||
loop {
|
||||
let res = cvar.wait_timeout(state, dur).unwrap();
|
||||
state = res.0;
|
||||
|
||||
if state.idle || res.1.timed_out() {
|
||||
// We received the notification and the value has been updated, we can leave.
|
||||
break;
|
||||
}
|
||||
if state.idle || res.1.timed_out() {
|
||||
// We received the notification and the value has been updated, we can leave.
|
||||
break;
|
||||
}
|
||||
state.idle = false;
|
||||
}
|
||||
state.idle = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,32 +718,111 @@ pub fn perform_inbox_jobs(context: &Context) {
|
||||
*context.probe_imap_network.write().unwrap() = false;
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = false;
|
||||
|
||||
job_perform(context, Thread::Imap, probe_imap_network);
|
||||
job_perform(context, ThreadExecutor::Inbox, probe_imap_network);
|
||||
info!(context, "dc_perform_inbox_jobs ended.",);
|
||||
}
|
||||
|
||||
pub fn perform_mvbox_jobs(context: &Context) {
|
||||
info!(context, "dc_perform_mbox_jobs EMPTY (for now).",);
|
||||
info!(context, "dc_perform_mbox_jobs starting.",);
|
||||
|
||||
let probe_imap_network = *context.probe_imap_network.clone().read().unwrap();
|
||||
*context.probe_imap_network.write().unwrap() = false;
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = false;
|
||||
|
||||
job_perform(context, ThreadExecutor::Mvbox, probe_imap_network);
|
||||
info!(context, "dc_perform_mbox_jobs ended.",);
|
||||
}
|
||||
|
||||
pub fn perform_sentbox_jobs(context: &Context) {
|
||||
info!(context, "dc_perform_sentbox_jobs EMPTY (for now).",);
|
||||
info!(context, "dc_perform_sentbox_jobs starting.",);
|
||||
|
||||
let probe_imap_network = *context.probe_imap_network.clone().read().unwrap();
|
||||
*context.probe_imap_network.write().unwrap() = false;
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = false;
|
||||
|
||||
job_perform(context, ThreadExecutor::Sentbox, probe_imap_network);
|
||||
info!(context, "dc_perform_sentbox_jobs ended.",);
|
||||
}
|
||||
|
||||
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
let jobs: Vec<Job> = load_jobs(context, thread, probe_network);
|
||||
fn job_perform(context: &Context, thread: ThreadExecutor, probe_network: bool) {
|
||||
let folder = get_folder_for_executor(context, thread);
|
||||
|
||||
for mut job in jobs {
|
||||
let fallback = if thread == ThreadExecutor::Inbox || thread == ThreadExecutor::Smtp {
|
||||
// only select non empty ones on the inbox thread or smtp
|
||||
"OR (j.foreign_id IS NULL)) "
|
||||
} else {
|
||||
") "
|
||||
};
|
||||
|
||||
let query_start = concat!(
|
||||
"SELECT ",
|
||||
"j.id AS id, ",
|
||||
"j.action AS action, ",
|
||||
"j.foreign_id AS foreign_id, ",
|
||||
"j.param AS param, ",
|
||||
"j.added_timestamp AS added_timestamp, ",
|
||||
"j.desired_timestamp AS desired_timestamp, ",
|
||||
"j.tries AS tries ",
|
||||
"FROM jobs j ",
|
||||
"LEFT JOIN msgs m ON j.foreign_id=m.id ",
|
||||
)
|
||||
.to_string();
|
||||
let query = if !probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
query_start
|
||||
+ "WHERE j.thread=? AND j.desired_timestamp<=? AND "
|
||||
+ "((j.foreign_id IS NOT NULL AND m.server_folder=?) "
|
||||
+ fallback
|
||||
+ "ORDER BY j.action DESC, j.added_timestamp;"
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
// in the order of their backoff-times.
|
||||
query_start
|
||||
+ "WHERE j.thread=? AND j.tries>0 AND "
|
||||
+ "((j.foreign_id IS NOT NULL AND m.server_folder=?) "
|
||||
+ fallback
|
||||
+ "ORDER BY j.desired_timestamp, j.action DESC;"
|
||||
};
|
||||
|
||||
let params_no_probe = params![thread as i64, time(), folder];
|
||||
let params_probe = params![thread as i64, folder];
|
||||
let params: &[&dyn rusqlite::ToSql] = if !probe_network {
|
||||
params_no_probe
|
||||
} else {
|
||||
params_probe
|
||||
};
|
||||
let jobs: Result<Vec<Job>, _> = context
|
||||
.sql
|
||||
.query_map(
|
||||
query,
|
||||
params,
|
||||
|row| {
|
||||
let job = Job {
|
||||
job_id: row.get(0)?,
|
||||
action: row.get(1)?,
|
||||
foreign_id: row.get(2)?,
|
||||
desired_timestamp: row.get(5)?,
|
||||
added_timestamp: row.get(4)?,
|
||||
tries: row.get(6)?,
|
||||
param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
|
||||
try_again: TryAgain::Dont,
|
||||
pending_error: None,
|
||||
};
|
||||
|
||||
Ok(job)
|
||||
},
|
||||
|jobs| jobs.collect::<Result<Vec<Job>, _>>().map_err(Into::into),
|
||||
)
|
||||
.map_err(|err| {
|
||||
warn!(context, "query failed: {:?}", err);
|
||||
});
|
||||
|
||||
for mut job in jobs.unwrap_or_default() {
|
||||
info!(
|
||||
context,
|
||||
"{}-job #{}, action {} started...",
|
||||
if thread == Thread::Imap {
|
||||
"INBOX"
|
||||
} else {
|
||||
"SMTP"
|
||||
},
|
||||
job.job_id,
|
||||
job.action,
|
||||
"{:?}-job #{}, action {} started...", thread, job.job_id, job.action,
|
||||
);
|
||||
|
||||
// some configuration jobs are "exclusive":
|
||||
@@ -762,31 +846,34 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
suspend_smtp_thread(context, true);
|
||||
}
|
||||
|
||||
for _tries in 0..2 {
|
||||
let mut tries = 0;
|
||||
while tries <= 1 {
|
||||
// this can be modified by a job using dc_job_try_again_later()
|
||||
job.try_again = TryAgain::Dont;
|
||||
|
||||
match job.action {
|
||||
Action::Unknown => {
|
||||
info!(context, "Unknown job id found");
|
||||
warn!(context, "Unknown job id found");
|
||||
}
|
||||
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
|
||||
Action::EmptyServer => job.EmptyServer(context),
|
||||
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
|
||||
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
|
||||
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
|
||||
Action::MoveMsg => job.MoveMsg(context),
|
||||
Action::SendMdn => job.SendMsgToSmtp(context),
|
||||
Action::ConfigureImap => JobConfigureImap(context),
|
||||
Action::ImexImap => match JobImexImap(context, &job) {
|
||||
Action::SendMsgToSmtp => job.do_DC_JOB_SEND(context),
|
||||
Action::EmptyServer => job.do_DC_JOB_EMPTY_SERVER(context, thread),
|
||||
Action::DeleteMsgOnImap => job.do_DC_JOB_DELETE_MSG_ON_IMAP(context, thread),
|
||||
Action::MarkseenMsgOnImap => job.do_DC_JOB_MARKSEEN_MSG_ON_IMAP(context, thread),
|
||||
Action::MarkseenMdnOnImap => job.do_DC_JOB_MARKSEEN_MDN_ON_IMAP(context, thread),
|
||||
Action::MoveMsg => job.do_DC_JOB_MOVE_MSG(context, thread),
|
||||
Action::SendMdn => job.do_DC_JOB_SEND(context),
|
||||
Action::ConfigureImap => dc_job_do_DC_JOB_CONFIGURE_IMAP(context),
|
||||
Action::ImexImap => match job_do_DC_JOB_IMEX_IMAP(context, &job) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
error!(context, "{}", err);
|
||||
}
|
||||
},
|
||||
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
|
||||
Action::MaybeSendLocations => {
|
||||
location::job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context, &job)
|
||||
}
|
||||
Action::MaybeSendLocationsEnded => {
|
||||
location::JobMaybeSendLocationsEnded(context, &mut job)
|
||||
location::job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context, &mut job)
|
||||
}
|
||||
Action::Housekeeping => sql::housekeeping(context),
|
||||
Action::SendMdnOld => {}
|
||||
@@ -795,6 +882,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
if job.try_again != TryAgain::AtOnce {
|
||||
break;
|
||||
}
|
||||
tries += 1
|
||||
}
|
||||
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
|
||||
context
|
||||
@@ -813,31 +901,28 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
break;
|
||||
} else if job.try_again == TryAgain::AtOnce || job.try_again == TryAgain::StandardDelay {
|
||||
let tries = job.tries + 1;
|
||||
if tries < JOB_RETRIES {
|
||||
if tries < 17 {
|
||||
job.tries = tries;
|
||||
let time_offset = get_backoff_time_offset(tries);
|
||||
job.desired_timestamp = time() + time_offset;
|
||||
job.desired_timestamp = job.added_timestamp + time_offset;
|
||||
job.update(context);
|
||||
info!(
|
||||
context,
|
||||
"{}-job #{} not succeeded on try #{}, retry in {} seconds.",
|
||||
if thread == Thread::Imap {
|
||||
"INBOX"
|
||||
} else {
|
||||
"SMTP"
|
||||
},
|
||||
"{:?}-job #{} not succeeded on try #{}, retry in ADD_TIME+{} (in {} seconds).",
|
||||
thread,
|
||||
job.job_id as u32,
|
||||
tries,
|
||||
time_offset
|
||||
time_offset,
|
||||
job.added_timestamp + time_offset - time()
|
||||
);
|
||||
if thread == Thread::Smtp && tries < JOB_RETRIES - 1 {
|
||||
if thread == ThreadExecutor::Smtp && tries < 17 - 1 {
|
||||
context
|
||||
.smtp_state
|
||||
.clone()
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.perform_jobs_needed = PerformJobsNeeded::AvoidDos;
|
||||
.perform_jobs_needed = 2;
|
||||
}
|
||||
} else {
|
||||
if job.action == Action::SendMsgToSmtp {
|
||||
@@ -863,11 +948,13 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_backoff_time_offset(tries: u32) -> i64 {
|
||||
let n = 2_i32.pow(tries - 1) * 60;
|
||||
#[allow(non_snake_case)]
|
||||
fn get_backoff_time_offset(c_tries: libc::c_int) -> i64 {
|
||||
// results in ~3 weeks for the last backoff timespan
|
||||
let N = 2_i32.pow((c_tries - 1) as u32) * 60;
|
||||
let mut rng = thread_rng();
|
||||
let r: i32 = rng.gen();
|
||||
let mut seconds = r % (n + 1);
|
||||
let n: i32 = rng.gen();
|
||||
let mut seconds = n % (N + 1);
|
||||
if seconds < 1 {
|
||||
seconds = 1;
|
||||
}
|
||||
@@ -956,122 +1043,76 @@ pub fn job_add(
|
||||
).ok();
|
||||
|
||||
match thread {
|
||||
Thread::Imap => interrupt_inbox_idle(context, false),
|
||||
Thread::Imap => {
|
||||
let folder: String = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
context,
|
||||
"SELECT server_folder FROM msgs WHERE id=?",
|
||||
params![foreign_id],
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mbox_thread = get_matching_mailbox(context, &folder);
|
||||
let res = mbox_thread.try_read();
|
||||
match res {
|
||||
Ok(mbox_thread) => {
|
||||
mbox_thread.interrupt_idle(context);
|
||||
}
|
||||
Err(err) => {
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = true;
|
||||
warn!(context, "could not interrupt idle: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Thread::Smtp => interrupt_smtp_idle(context),
|
||||
Thread::Unknown => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_folder_for_executor(context: &Context, thread: ThreadExecutor) -> String {
|
||||
let t = get_mailbox_for_executor(context, thread);
|
||||
let b = t.read().unwrap();
|
||||
b.get_watch_folder(context).unwrap_or_default().to_string()
|
||||
}
|
||||
|
||||
fn get_mailbox_for_executor(context: &Context, thread: ThreadExecutor) -> Arc<RwLock<JobThread>> {
|
||||
match thread {
|
||||
ThreadExecutor::Inbox => context.inbox_thread.clone(),
|
||||
ThreadExecutor::Mvbox => context.mvbox_thread.clone(),
|
||||
ThreadExecutor::Sentbox => context.sentbox_thread.clone(),
|
||||
ThreadExecutor::Smtp => panic!("do not use for smtp"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_matching_mailbox(context: &Context, folder: &String) -> Arc<RwLock<JobThread>> {
|
||||
let mvbox_folder = context
|
||||
.mvbox_thread
|
||||
.read()
|
||||
.unwrap()
|
||||
.get_watch_folder(context);
|
||||
let sentbox_folder = context
|
||||
.sentbox_thread
|
||||
.read()
|
||||
.unwrap()
|
||||
.get_watch_folder(context);
|
||||
|
||||
if mvbox_folder.is_some() && folder == mvbox_folder.as_ref().unwrap() {
|
||||
context.mvbox_thread.clone()
|
||||
} else if sentbox_folder.is_some() && folder == sentbox_folder.as_ref().unwrap() {
|
||||
context.sentbox_thread.clone()
|
||||
} else {
|
||||
context.inbox_thread.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_smtp_idle(context: &Context) {
|
||||
info!(context, "Interrupting SMTP-idle...",);
|
||||
|
||||
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
state.perform_jobs_needed = PerformJobsNeeded::AtOnce;
|
||||
state.perform_jobs_needed = 1;
|
||||
state.idle = true;
|
||||
cvar.notify_one();
|
||||
info!(context, "Interrupting SMTP-idle... ended",);
|
||||
}
|
||||
|
||||
/// Load jobs from the database.
|
||||
///
|
||||
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
|
||||
/// IMAP jobs. The `probe_network` parameter decides how to query
|
||||
/// jobs, this is tricky and probably wrong currently. Look at the
|
||||
/// SQL queries for details.
|
||||
fn load_jobs(context: &Context, thread: Thread, probe_network: bool) -> Vec<Job> {
|
||||
let query = if !probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
// process jobs in the order they were added.
|
||||
"SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries \
|
||||
FROM jobs WHERE thread=? AND desired_timestamp<=? ORDER BY action DESC, added_timestamp;"
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
// in the order of their backoff-times.
|
||||
"SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries \
|
||||
FROM jobs WHERE thread=? AND tries>0 ORDER BY desired_timestamp, action DESC;"
|
||||
};
|
||||
|
||||
let params_no_probe = params![thread as i64, time()];
|
||||
let params_probe = params![thread as i64];
|
||||
let params: &[&dyn rusqlite::ToSql] = if !probe_network {
|
||||
params_no_probe
|
||||
} else {
|
||||
params_probe
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
query,
|
||||
params,
|
||||
|row| {
|
||||
let job = Job {
|
||||
job_id: row.get(0)?,
|
||||
action: row.get(1)?,
|
||||
foreign_id: row.get(2)?,
|
||||
desired_timestamp: row.get(5)?,
|
||||
added_timestamp: row.get(4)?,
|
||||
tries: row.get(6)?,
|
||||
param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
|
||||
try_again: TryAgain::Dont,
|
||||
pending_error: None,
|
||||
};
|
||||
|
||||
Ok(job)
|
||||
},
|
||||
|jobs| {
|
||||
let mut ret: Vec<Job> = Vec::new();
|
||||
for job in jobs {
|
||||
match job {
|
||||
Ok(j) => ret.push(j),
|
||||
Err(e) => warn!(context, "Bad job from the database: {}", e),
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::*;
|
||||
|
||||
fn insert_job(context: &Context, foreign_id: i64) {
|
||||
let now = time();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
params![
|
||||
now,
|
||||
Thread::from(Action::MoveMsg),
|
||||
Action::MoveMsg,
|
||||
foreign_id,
|
||||
Params::new().to_string(),
|
||||
now
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_jobs() {
|
||||
// We want to ensure that loading jobs skips over jobs which
|
||||
// fails to load from the database instead of failing to load
|
||||
// all jobs.
|
||||
let t = dummy_context();
|
||||
insert_job(&t.ctx, 0);
|
||||
insert_job(&t.ctx, -1); // This can not be loaded into Job struct.
|
||||
insert_job(&t.ctx, 1);
|
||||
let jobs = load_jobs(&t.ctx, Thread::from(Action::MoveMsg), false);
|
||||
assert_eq!(jobs.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ impl JobThread {
|
||||
|
||||
state.idle = true;
|
||||
cvar.notify_one();
|
||||
info!(context, "Interrupting {}-IDLE... finished", self.name);
|
||||
}
|
||||
|
||||
pub fn fetch(&mut self, context: &Context, use_network: bool) {
|
||||
@@ -104,7 +103,7 @@ impl JobThread {
|
||||
if let Some(watch_folder) = self.get_watch_folder(context) {
|
||||
let start = std::time::Instant::now();
|
||||
info!(context, "{} started...", prefix);
|
||||
let res = self.imap.fetch(context, &watch_folder).map_err(Into::into);
|
||||
let res = self.imap.fetch(context, &watch_folder);
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
|
||||
|
||||
@@ -113,11 +112,11 @@ impl JobThread {
|
||||
Err(Error::WatchFolderNotFound("not-set".to_string()))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(crate::error::Error::Message(err.to_string())),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_watch_folder(&self, context: &Context) -> Option<String> {
|
||||
pub(crate) fn get_watch_folder(&self, context: &Context) -> Option<String> {
|
||||
match context.sql.get_raw_config(context, self.folder_config_name) {
|
||||
Some(name) => Some(name),
|
||||
None => {
|
||||
@@ -176,7 +175,7 @@ impl JobThread {
|
||||
info!(context, "{} ended...", prefix);
|
||||
match res {
|
||||
Ok(()) => false,
|
||||
Err(crate::imap::Error::IdleAbilityMissing) => true, // we have to do fake_idle
|
||||
Err(Error::ImapMissesIdle) => true, // we have to do fake_idle
|
||||
Err(err) => {
|
||||
warn!(context, "{} failed: {} -> reconnecting", prefix, err);
|
||||
// something is borked, let's start afresh on the next occassion
|
||||
|
||||
13
src/key.rs
13
src/key.rs
@@ -217,18 +217,15 @@ impl Key {
|
||||
.expect("failed to serialize key")
|
||||
}
|
||||
|
||||
pub fn write_asc_to_file(
|
||||
&self,
|
||||
file: impl AsRef<Path>,
|
||||
context: &Context,
|
||||
) -> std::io::Result<()> {
|
||||
pub fn write_asc_to_file(&self, file: impl AsRef<Path>, context: &Context) -> bool {
|
||||
let file_content = self.to_asc(None).into_bytes();
|
||||
|
||||
let res = dc_write_file(context, &file, &file_content);
|
||||
if res.is_err() {
|
||||
if dc_write_file(context, &file, &file_content) {
|
||||
true
|
||||
} else {
|
||||
error!(context, "Cannot write key to {}", file.as_ref().display());
|
||||
false
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> String {
|
||||
|
||||
@@ -538,7 +538,7 @@ pub fn save(
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
|
||||
pub fn job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) {
|
||||
let now = time();
|
||||
let mut continue_streaming = false;
|
||||
info!(
|
||||
@@ -628,7 +628,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) {
|
||||
pub fn job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) {
|
||||
// this function is called when location-streaming _might_ have ended for a chat.
|
||||
// the function checks, if location-streaming is really ended;
|
||||
// if so, a device-message is added if not yet done.
|
||||
|
||||
12
src/log.rs
12
src/log.rs
@@ -5,10 +5,10 @@ macro_rules! info {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
info!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::Info(formatted));
|
||||
}};
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
@@ -16,10 +16,10 @@ macro_rules! warn {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
warn!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::Warning(formatted));
|
||||
}};
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
@@ -27,10 +27,10 @@ macro_rules! error {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
error!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::Event::Error(formatted));
|
||||
}};
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
||||
@@ -159,6 +159,7 @@ pub struct Message {
|
||||
pub(crate) from_id: u32,
|
||||
pub(crate) to_id: u32,
|
||||
pub(crate) chat_id: u32,
|
||||
pub(crate) move_state: MoveState,
|
||||
pub(crate) type_0: Viewtype,
|
||||
pub(crate) state: MessageState,
|
||||
pub(crate) hidden: bool,
|
||||
@@ -199,6 +200,7 @@ impl Message {
|
||||
" m.mime_in_reply_to AS mime_in_reply_to,",
|
||||
" m.server_folder AS server_folder,",
|
||||
" m.server_uid AS server_uid,",
|
||||
" m.move_state as move_state,",
|
||||
" m.chat_id AS chat_id,",
|
||||
" m.from_id AS from_id,",
|
||||
" m.to_id AS to_id,",
|
||||
@@ -226,6 +228,7 @@ impl Message {
|
||||
msg.in_reply_to = row.get::<_, Option<String>>("mime_in_reply_to")?;
|
||||
msg.server_folder = row.get::<_, Option<String>>("server_folder")?;
|
||||
msg.server_uid = row.get("server_uid")?;
|
||||
msg.move_state = row.get("move_state")?;
|
||||
msg.chat_id = row.get("chat_id")?;
|
||||
msg.from_id = row.get("from_id")?;
|
||||
msg.to_id = row.get("to_id")?;
|
||||
@@ -1071,6 +1074,18 @@ pub fn exists(context: &Context, msg_id: MsgId) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_msg_move_state(context: &Context, rfc724_mid: &str, state: MoveState) -> bool {
|
||||
// we update the move_state for all messages belonging to a given Message-ID
|
||||
// so that the state stay intact when parts are deleted
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"UPDATE msgs SET move_state=? WHERE rfc724_mid=?;",
|
||||
params![state as i32, rfc724_mid],
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl AsRef<str>>) {
|
||||
if let Ok(mut msg) = Message::load_from_db(context, msg_id) {
|
||||
if msg.state.can_fail() {
|
||||
|
||||
@@ -230,7 +230,7 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
// 1=add Autocrypt-header (needed eg. for handshaking), 2=no Autocrypte-header (used for MDN)
|
||||
let mut e2ee_guaranteed = false;
|
||||
let mut min_verified = crate::peerstate::PeerstateVerifiedStatus::Unverified;
|
||||
let mut min_verified: libc::c_int = 0;
|
||||
let mut do_gossip = false;
|
||||
let mut grpimage = None;
|
||||
let force_plaintext: libc::c_int;
|
||||
@@ -246,7 +246,7 @@ impl<'a> MimeFactory<'a> {
|
||||
wrapmime::new_custom_field(imf_fields, "Chat-Verified", "1");
|
||||
force_plaintext = 0;
|
||||
e2ee_guaranteed = true;
|
||||
min_verified = crate::peerstate::PeerstateVerifiedStatus::BidirectVerified;
|
||||
min_verified = 2
|
||||
} else {
|
||||
force_plaintext = self
|
||||
.msg
|
||||
@@ -707,7 +707,7 @@ impl<'a> MimeFactory<'a> {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !email_to_remove.is_empty() && !addr_cmp(email_to_remove, self_addr) {
|
||||
if !email_to_remove.is_empty() && email_to_remove != self_addr {
|
||||
if !vec_contains_lowercase(&factory.recipients_addr, &email_to_remove) {
|
||||
factory.recipients_names.push("".to_string());
|
||||
factory.recipients_addr.push(email_to_remove.to_string());
|
||||
|
||||
@@ -355,8 +355,6 @@ fn normalize_addr(addr: &str) -> &str {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[test]
|
||||
fn test_normalize_addr() {
|
||||
assert_eq!(normalize_addr(" hello@mail.de "), "hello@mail.de");
|
||||
@@ -386,24 +384,4 @@ mod tests {
|
||||
|
||||
assert_eq!(Oauth2::from_address("hello@web.de"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_addr() {
|
||||
let ctx = dummy_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code);
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_token() {
|
||||
let ctx = dummy_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false);
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,7 @@ mod tests {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::blob::BlobErrorKind;
|
||||
use crate::test_utils::*;
|
||||
|
||||
#[test]
|
||||
@@ -403,10 +404,7 @@ mod tests {
|
||||
|
||||
// Blob does not exist yet, expect BlobError.
|
||||
let err = p.get_blob(Param::File, &t.ctx, false).unwrap_err();
|
||||
match err {
|
||||
BlobError::WrongBlobdir { .. } => (),
|
||||
_ => panic!("wrong error type/variant: {:?}", err),
|
||||
}
|
||||
assert_eq!(err.kind(), BlobErrorKind::WrongBlobdir);
|
||||
|
||||
fs::write(fname, b"boo").unwrap();
|
||||
let blob = p.get_blob(Param::File, &t.ctx, true).unwrap().unwrap();
|
||||
|
||||
@@ -12,17 +12,6 @@ use crate::error::*;
|
||||
use crate::key::*;
|
||||
use crate::sql::{self, Sql};
|
||||
|
||||
pub const DC_PS_GOSSIP_KEY: u32 = 0;
|
||||
pub const DC_PS_PUBLIC_KEY: u32 = 1;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum PeerstateVerifiedStatus {
|
||||
Unverified = 0,
|
||||
//Verified = 1, // not used
|
||||
BidirectVerified = 2,
|
||||
}
|
||||
|
||||
/// Peerstate represents the state of an Autocrypt peer.
|
||||
pub struct Peerstate<'a> {
|
||||
pub context: &'a Context,
|
||||
@@ -322,7 +311,7 @@ impl<'a> Peerstate<'a> {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
|
||||
pub fn render_gossip_header(&self, min_verified: usize) -> Option<String> {
|
||||
if let Some(ref addr) = self.addr {
|
||||
if let Some(key) = self.peek_key(min_verified) {
|
||||
// TODO: avoid cloning
|
||||
@@ -338,12 +327,12 @@ impl<'a> Peerstate<'a> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
|
||||
pub fn peek_key(&self, min_verified: usize) -> Option<&Key> {
|
||||
if self.public_key.is_none() && self.gossip_key.is_none() && self.verified_key.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if min_verified != PeerstateVerifiedStatus::Unverified {
|
||||
if 0 != min_verified {
|
||||
return self.verified_key.as_ref();
|
||||
}
|
||||
if self.public_key.is_some() {
|
||||
@@ -353,17 +342,10 @@ impl<'a> Peerstate<'a> {
|
||||
self.gossip_key.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_verified(
|
||||
&mut self,
|
||||
which_key: u32,
|
||||
fingerprint: &str,
|
||||
verified: PeerstateVerifiedStatus,
|
||||
) -> bool {
|
||||
pub fn set_verified(&mut self, which_key: usize, fingerprint: &str, verified: usize) -> bool {
|
||||
let mut success = false;
|
||||
if !(which_key != DC_PS_GOSSIP_KEY && which_key != DC_PS_PUBLIC_KEY
|
||||
|| verified != PeerstateVerifiedStatus::BidirectVerified)
|
||||
{
|
||||
if which_key == DC_PS_PUBLIC_KEY
|
||||
if !(which_key != 0 && which_key != 1 || verified != 2) {
|
||||
if which_key == 1
|
||||
&& self.public_key_fingerprint.is_some()
|
||||
&& self.public_key_fingerprint.as_ref().unwrap() == fingerprint
|
||||
{
|
||||
@@ -372,7 +354,7 @@ impl<'a> Peerstate<'a> {
|
||||
self.verified_key_fingerprint = self.public_key_fingerprint.clone();
|
||||
success = true;
|
||||
}
|
||||
if which_key == DC_PS_GOSSIP_KEY
|
||||
if which_key == 0
|
||||
&& self.gossip_key_fingerprint.is_some()
|
||||
&& self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint
|
||||
{
|
||||
|
||||
@@ -242,8 +242,7 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
|
||||
|
||||
// Bob -> Alice
|
||||
while !context.shall_stop_ongoing() {
|
||||
// Don't sleep too long, the user is waiting.
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
std::thread::sleep(std::time::Duration::new(0, 3_000_000));
|
||||
}
|
||||
cleanup(&context, contact_chat_id, true, join_vg)
|
||||
}
|
||||
@@ -319,40 +318,21 @@ fn fingerprint_equals_sender(
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) struct HandshakeMessageStatus {
|
||||
pub(crate) hide_this_msg: bool,
|
||||
pub(crate) delete_this_msg: bool,
|
||||
pub(crate) stop_ongoing_process: bool,
|
||||
pub(crate) bob_securejoin_success: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for HandshakeMessageStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hide_this_msg: true,
|
||||
delete_this_msg: false,
|
||||
stop_ongoing_process: false,
|
||||
bob_securejoin_success: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* library private: secure-join */
|
||||
pub(crate) fn handle_securejoin_handshake(
|
||||
pub fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mimeparser: &MimeParser,
|
||||
contact_id: u32,
|
||||
) -> Result<HandshakeMessageStatus, Error> {
|
||||
) -> libc::c_int {
|
||||
let own_fingerprint: String;
|
||||
|
||||
ensure!(
|
||||
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
|
||||
"handle_securejoin_handshake(): called with special contact id"
|
||||
);
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return 0;
|
||||
}
|
||||
let step = match mimeparser.lookup_optional_field("Secure-Join") {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
bail!("This message is not a Secure-Join message");
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
info!(
|
||||
@@ -365,8 +345,8 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
if contact_chat_id_blocked != Blocked::Not {
|
||||
chat::unblock(context, contact_chat_id);
|
||||
}
|
||||
let mut ret: libc::c_int = DC_HANDSHAKE_STOP_NORMAL_PROCESSING;
|
||||
let join_vg = step.starts_with("vg-");
|
||||
let mut ret = HandshakeMessageStatus::default();
|
||||
|
||||
match step.as_str() {
|
||||
"vg-request" | "vc-request" => {
|
||||
@@ -382,12 +362,12 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
Some(n) => n,
|
||||
None => {
|
||||
warn!(context, "Secure-join denied (invitenumber missing).",);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if !token::exists(context, token::Namespace::InviteNumber, &invitenumber) {
|
||||
warn!(context, "Secure-join denied (bad invitenumber).",);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
info!(context, "Secure-join requested.",);
|
||||
|
||||
@@ -413,7 +393,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
if cond {
|
||||
warn!(context, "auth-required message out of sync.",);
|
||||
// no error, just aborted somehow or a mail from another handshake
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
|
||||
let auth = get_qr_attr!(context, auth).to_string();
|
||||
@@ -428,9 +408,8 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
"Not encrypted."
|
||||
},
|
||||
);
|
||||
ret.stop_ongoing_process = true;
|
||||
ret.bob_securejoin_success = Some(false);
|
||||
return Ok(ret);
|
||||
end_bobs_joining(context, DC_BOB_ERROR);
|
||||
return ret;
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) {
|
||||
could_not_establish_secure_connection(
|
||||
@@ -438,9 +417,8 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
);
|
||||
ret.stop_ongoing_process = true;
|
||||
ret.bob_securejoin_success = Some(false);
|
||||
return Ok(ret);
|
||||
end_bobs_joining(context, DC_BOB_ERROR);
|
||||
return ret;
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
own_fingerprint = get_self_fingerprint(context).unwrap();
|
||||
@@ -475,7 +453,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if !encrypted_and_signed(mimeparser, &fingerprint) {
|
||||
@@ -484,7 +462,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Auth not encrypted.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) {
|
||||
could_not_establish_secure_connection(
|
||||
@@ -492,7 +470,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
|
||||
@@ -504,12 +482,12 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Auth not provided.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if !token::exists(context, token::Namespace::Auth, &auth_0) {
|
||||
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.");
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
if mark_peer_as_verified(context, fingerprint).is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
@@ -517,7 +495,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited);
|
||||
info!(context, "Auth verified.",);
|
||||
@@ -531,7 +509,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
|
||||
if group_chat_id == 0 {
|
||||
error!(context, "Chat {} not found.", &field_grpid);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
} else {
|
||||
if let Err(err) =
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
|
||||
@@ -546,11 +524,11 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
}
|
||||
"vg-member-added" | "vc-contact-confirm" => {
|
||||
if join_vg {
|
||||
ret.hide_this_msg = false;
|
||||
ret = DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING;
|
||||
}
|
||||
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM {
|
||||
info!(context, "Message belongs to a different handshake.",);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
let cond = {
|
||||
let bob = context.bob.read().unwrap();
|
||||
@@ -562,7 +540,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
context,
|
||||
"Message out of sync or belongs to a different handshake.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
|
||||
|
||||
@@ -586,8 +564,8 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Contact confirm message not encrypted.",
|
||||
);
|
||||
ret.bob_securejoin_success = Some(false);
|
||||
return Ok(ret);
|
||||
end_bobs_joining(context, DC_BOB_ERROR);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
|
||||
@@ -596,7 +574,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
|
||||
emit_event!(context, Event::ContactsChanged(None));
|
||||
@@ -605,7 +583,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
.unwrap_or_default();
|
||||
if join_vg && !addr_equals_self(context, cg_member_added) {
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id);
|
||||
context.bob.write().unwrap().expects = 0;
|
||||
@@ -619,8 +597,7 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
"",
|
||||
);
|
||||
}
|
||||
ret.stop_ongoing_process = true;
|
||||
ret.bob_securejoin_success = Some(true);
|
||||
end_bobs_joining(context, DC_BOB_SUCCESS);
|
||||
}
|
||||
"vg-member-added-received" => {
|
||||
/* ============================================================
|
||||
@@ -630,31 +607,28 @@ pub(crate) fn handle_securejoin_handshake(
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
|
||||
if contact.is_verified(context) == VerifiedStatus::Unverified {
|
||||
warn!(context, "vg-member-added-received invalid.",);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
let field_grpid = mimeparser
|
||||
.lookup_optional_field("Secure-Join-Group")
|
||||
.unwrap_or_default();
|
||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
|
||||
context.call_cb(Event::SecurejoinMemberAdded {
|
||||
chat_id: group_chat_id,
|
||||
contact_id: contact_id,
|
||||
});
|
||||
} else {
|
||||
warn!(context, "vg-member-added-received invalid.",);
|
||||
return Ok(ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
warn!(context, "invalid step: {}", step);
|
||||
}
|
||||
}
|
||||
if ret.hide_this_msg {
|
||||
ret.delete_this_msg = true;
|
||||
if ret == DC_HANDSHAKE_STOP_NORMAL_PROCESSING {
|
||||
ret |= DC_HANDSHAKE_ADD_DELETE_JOB;
|
||||
}
|
||||
Ok(ret)
|
||||
ret
|
||||
}
|
||||
|
||||
fn end_bobs_joining(context: &Context, status: libc::c_int) {
|
||||
context.bob.write().unwrap().status = status;
|
||||
context.stop_ongoing();
|
||||
}
|
||||
|
||||
fn secure_connection_established(context: &Context, contact_chat_id: u32) {
|
||||
@@ -690,11 +664,7 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
|
||||
if let Some(ref mut peerstate) =
|
||||
Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref())
|
||||
{
|
||||
if peerstate.set_verified(
|
||||
DC_PS_PUBLIC_KEY,
|
||||
fingerprint.as_ref(),
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
) {
|
||||
if peerstate.set_verified(1, fingerprint.as_ref(), 2) {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.to_save = Some(ToSave::All);
|
||||
peerstate
|
||||
|
||||
50
src/smtp.rs
50
src/smtp.rs
@@ -1,8 +1,6 @@
|
||||
use lettre::smtp::client::net::*;
|
||||
use lettre::*;
|
||||
|
||||
use failure::Fail;
|
||||
|
||||
use crate::constants::*;
|
||||
use crate::context::Context;
|
||||
use crate::error::Error;
|
||||
@@ -19,16 +17,6 @@ pub struct Smtp {
|
||||
from: Option<EmailAddress>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum SmtpError {
|
||||
#[fail(display = "Envelope error: {}", _0)]
|
||||
EnvelopeError(#[cause] lettre::error::Error),
|
||||
#[fail(display = "Send error: {}", _0)]
|
||||
SendError(#[cause] lettre::smtp::error::Error),
|
||||
#[fail(display = "SMTP has no transport")]
|
||||
NoTransport,
|
||||
}
|
||||
|
||||
impl Smtp {
|
||||
/// Create a new Smtp instances.
|
||||
pub fn new() -> Self {
|
||||
@@ -156,7 +144,7 @@ impl Smtp {
|
||||
recipients: Vec<EmailAddress>,
|
||||
message: Vec<u8>,
|
||||
job_id: u32,
|
||||
) -> Result<(), SmtpError> {
|
||||
) -> Result<(), Error> {
|
||||
let message_len = message.len();
|
||||
|
||||
let recipients_display = recipients
|
||||
@@ -166,28 +154,36 @@ impl Smtp {
|
||||
.join(",");
|
||||
|
||||
if let Some(ref mut transport) = self.transport {
|
||||
let envelope =
|
||||
Envelope::new(self.from.clone(), recipients).map_err(SmtpError::EnvelopeError)?;
|
||||
let envelope = match Envelope::new(self.from.clone(), recipients) {
|
||||
Ok(env) => env,
|
||||
Err(err) => {
|
||||
bail!("{}", err);
|
||||
}
|
||||
};
|
||||
let mail = SendableEmail::new(
|
||||
envelope,
|
||||
format!("{}", job_id), // only used for internal logging
|
||||
message,
|
||||
);
|
||||
|
||||
transport.send(mail).map_err(SmtpError::SendError)?;
|
||||
|
||||
context.call_cb(Event::SmtpMessageSent(format!(
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len, recipients_display
|
||||
)));
|
||||
self.transport_connected = true;
|
||||
Ok(())
|
||||
match transport.send(mail) {
|
||||
Ok(_) => {
|
||||
context.call_cb(Event::SmtpMessageSent(format!(
|
||||
"Message len={} was smtp-sent to {}",
|
||||
message_len, recipients_display
|
||||
)));
|
||||
self.transport_connected = true;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("SMTP failed len={}: error: {}", message_len, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"uh? SMTP has no transport, failed to send to {}", recipients_display
|
||||
bail!(
|
||||
"uh? SMTP has no transport, failed to send to {:?}",
|
||||
recipients_display
|
||||
);
|
||||
return Err(SmtpError::NoTransport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,7 +729,6 @@ fn open(
|
||||
}
|
||||
if dbversion < 48 {
|
||||
info!(context, "[migration] v48");
|
||||
// NOTE: move_state is not used anymore
|
||||
sql.execute(
|
||||
"ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
|
||||
params![],
|
||||
@@ -829,14 +828,6 @@ fn open(
|
||||
update_icons = true;
|
||||
sql.set_raw_config_int(context, "dbversion", 59)?;
|
||||
}
|
||||
if dbversion < 60 {
|
||||
info!(context, "[migration] v60");
|
||||
sql.execute(
|
||||
"ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
sql.set_raw_config_int(context, "dbversion", 60)?;
|
||||
}
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
|
||||
@@ -128,7 +128,7 @@ fn test_encryption_decryption() {
|
||||
public_keyring.add_ref(&public_key);
|
||||
|
||||
let mut public_keyring2 = Keyring::default();
|
||||
public_keyring2.add_owned(public_key2);
|
||||
public_keyring2.add_owned(public_key2.clone());
|
||||
|
||||
let mut valid_signatures: HashSet<String> = Default::default();
|
||||
|
||||
@@ -220,7 +220,7 @@ fn create_test_context() -> TestContext {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap();
|
||||
TestContext { ctx, dir }
|
||||
TestContext { ctx: ctx, dir: dir }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -233,6 +233,26 @@ fn test_dc_get_oauth2_url() {
|
||||
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_addr() {
|
||||
let ctx = create_test_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code);
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dc_get_oauth2_token() {
|
||||
let ctx = create_test_context();
|
||||
let addr = "dignifiedquire@gmail.com";
|
||||
let code = "fail";
|
||||
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false);
|
||||
// this should fail as it is an invalid password
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stress_tests() {
|
||||
unsafe {
|
||||
|
||||
Reference in New Issue
Block a user