Compare commits

..

17 Commits

Author SHA1 Message Date
Simon Laux
5b5c900cb9 also export qoutes, message state and is_forwarded 2021-06-01 23:58:04 +02:00
Simon Laux
b87ca6e747 restructure code 2021-06-01 23:58:04 +02:00
Simon Laux
850f7e1174 output daymarker and message error 2021-06-01 23:58:04 +02:00
Simon Laux
9f2b5feda2 move getting and writing message info to packing-
step, this could decrease memory usage
2021-06-01 23:58:04 +02:00
Simon Laux
b8cbcc6648 add export_timestamp to chat.json 2021-06-01 23:58:04 +02:00
Simon Laux
584d28f807 update code that it compiles with current core
make contact deduping more reliable
2021-06-01 23:58:04 +02:00
Simon Laux
241111470f export locations 2021-06-01 23:58:03 +02:00
Simon Laux
4bf07ccc71 repl export-chat-requires destination path
and combind the public accessible functions into one
2021-06-01 23:58:03 +02:00
Simon Laux
897d2f4a08 add some ducumentation 2021-06-01 23:58:03 +02:00
Simon Laux
a81096aa36 rename files in exported zip 2021-06-01 23:58:03 +02:00
Simon Laux
b1c9342631 save raw mime headers under the eml
extention instead of the text extention
2021-06-01 23:58:03 +02:00
Simon Laux
0c8aad2102 export message mime-headers when availible 2021-06-01 23:58:03 +02:00
Simon Laux
82253e1e30 fix deduplication of blob files 2021-06-01 23:58:03 +02:00
Simon Laux
aa953687bf add message info 2021-06-01 23:58:03 +02:00
Simon Laux
9f2f2ca1c0 [works again] export json, adjust to work in async
and remove html stuff
2021-06-01 23:58:03 +02:00
Simon Laux
54637004cd [still broken]* A bit progress on:
- transforming the export format to json
- async
- cleanup cod
(*broken state, just a commit to save progress)
2021-06-01 23:58:03 +02:00
Simon Laux
da9f45d9ff state of previous export chat pr 2021-06-01 23:58:01 +02:00
37 changed files with 880 additions and 1025 deletions

View File

@@ -1,30 +1,5 @@
# Changelog
## 1.56.0
- fix downscaling images #2469
- fix outgoing messages popping up in selfchat #2456
- securejoin: display error reason if there is any #2470
- do not allow deleting contacts with ongoing chats #2458
- fix: ignore drafts folder when scanning #2454
- fix: scan folders also when inbox is not watched #2446
- more robust In-Reply-To parsing #2182
- update dependencies #2441 #2438 #2439 #2440 #2447 #2448 #2449 #2452 #2453 #2460 #2464 #2466
- update provider-database #2471
- refactorings #2459 #2457
- improve tests and ci #2445 #2450 #2451
## 1.55.0
- fix panic when receiving some HTML messages #2434

76
Cargo.lock generated
View File

@@ -138,9 +138,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.41"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]]
name = "arrayvec"
@@ -637,6 +637,27 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bzip2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.10+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cache-padded"
version = "1.1.1"
@@ -865,7 +886,7 @@ dependencies = [
"clap",
"criterion-plot",
"csv",
"itertools 0.10.1",
"itertools 0.10.0",
"lazy_static",
"num-traits",
"oorandom",
@@ -1108,7 +1129,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.56.0"
version = "1.55.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1136,7 +1157,7 @@ dependencies = [
"hex",
"image",
"indexmap",
"itertools 0.10.1",
"itertools 0.10.0",
"kamadak-exif",
"lettre_email",
"libc",
@@ -1175,6 +1196,7 @@ dependencies = [
"toml",
"url",
"uuid",
"zip",
]
[[package]]
@@ -1187,7 +1209,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.56.0"
version = "1.55.0"
dependencies = [
"anyhow",
"async-std",
@@ -1636,9 +1658,9 @@ checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
[[package]]
name = "futures-lite"
version = "1.12.0"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb"
dependencies = [
"fastrand",
"futures-core",
@@ -2028,9 +2050,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
@@ -2128,9 +2150,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.97"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
[[package]]
name = "libm"
@@ -2449,9 +2471,9 @@ checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
[[package]]
name = "once_cell"
version = "1.8.0"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "oorandom"
@@ -3593,15 +3615,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "strum"
version = "0.21.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
[[package]]
name = "strum_macros"
version = "0.21.1"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
dependencies = [
"heck",
"proc-macro2",
@@ -3636,9 +3658,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.73"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
"proc-macro2",
"quote",
@@ -4212,3 +4234,17 @@ dependencies = [
"syn",
"synstructure",
]
[[package]]
name = "zip"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c83dc9b784d252127720168abd71ea82bf8c3d96b17dc565b5e2a02854f2b27"
dependencies = [
"byteorder",
"bzip2",
"crc32fast",
"flate2",
"thiserror",
"time 0.1.44",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.56.0"
version = "1.55.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -15,7 +15,7 @@ lto = true
deltachat_derive = { path = "./deltachat_derive" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.41"
anyhow = "1.0.40"
async-imap = "0.5.0"
async-native-tls = { version = "0.3.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
@@ -37,17 +37,17 @@ futures = "0.3.15"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
indexmap = "1.3.0"
itertools = "0.10.1"
itertools = "0.10.0"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2.97"
libc = "0.2.95"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.4"
native-tls = "0.2.3"
num_cpus = "1.13.0"
num-derive = "0.3.0"
num-traits = "0.2.6"
once_cell = "1.8.0"
once_cell = "1.4.1"
percent-encoding = "2.0"
pgp = { version = "0.7.0", default-features = false }
pretty_env_logger = { version = "0.4.0", optional = true }
@@ -66,19 +66,20 @@ sha-1 = "0.9.6"
sha2 = "0.9.5"
smallvec = "1.0.0"
stop-token = "0.2.0"
strum = "0.21.0"
strum_macros = "0.21.1"
strum = "0.20.0"
strum_macros = "0.20.1"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
thiserror = "1.0.25"
toml = "0.5.6"
url = "2.2.2"
uuid = { version = "0.8", features = ["serde", "v4"] }
zip = "0.5.12"
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.12.0"
futures-lite = "1.7.0"
log = "0.4.11"
pretty_assertions = "0.7.2"
pretty_env_logger = "0.4.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.56.0"
version = "1.55.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -21,7 +21,7 @@ human-panic = "1.0.1"
num-traits = "0.2.6"
serde_json = "1.0"
async-std = "1.9.0"
anyhow = "1.0.41"
anyhow = "1.0.40"
thiserror = "1.0.25"
rand = "0.7.3"

View File

@@ -1737,13 +1737,9 @@ pub unsafe extern "C" fn dc_block_contact(
let ctx = &*context;
block_on(async move {
if block == 0 {
Contact::unblock(&ctx, contact_id)
.await
.ok_or_log_msg(&ctx, "Can't unblock contact");
Contact::unblock(&ctx, contact_id).await;
} else {
Contact::block(&ctx, contact_id)
.await
.ok_or_log_msg(&ctx, "Can't block contact");
Contact::block(&ctx, contact_id).await;
}
});
}

View File

@@ -9,5 +9,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "1.0.73"
syn = "1.0.72"
quote = "1.0.2"

View File

@@ -13,6 +13,8 @@ use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::export_chat::export_chat_to_zip;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
@@ -372,7 +374,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
getlocations [<contact-id>]\n\
send <text>\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
videochat\n\
@@ -388,6 +389,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
export-chat <chat-id> <destination-file>\n\
===========================Contact requests==\n\
decidestartchat <msg-id>\n\
decideblock <msg-id>\n\
@@ -866,14 +868,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
}
"sendimage" | "sendsticker" | "sendfile" => {
"sendimage" | "sendfile" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No file given.");
let mut msg = Message::new(if arg0 == "sendimage" {
Viewtype::Image
} else if arg0 == "sendsticker" {
Viewtype::Sticker
} else {
Viewtype::File
});
@@ -1025,6 +1025,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(&context).await?;
}
"export-chat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
ensure!(!arg2.is_empty(), "Argument <destination file> missing.");
let chat_id = ChatId::new(arg1.parse()?);
// todo check if path is valid (dest dir exists) and ends in .zip
export_chat_to_zip(&context, chat_id, arg2).await;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
@@ -1137,12 +1144,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::block(&context, contact_id).await?;
Contact::block(&context, contact_id).await;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::unblock(&context, contact_id).await?;
Contact::unblock(&context, contact_id).await;
}
"listblocked" => {
let contacts = Contact::get_all_blocked(&context).await?;

View File

@@ -169,7 +169,7 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 34] = [
const CHAT_COMMANDS: [&str; 35] = [
"listchats",
"listarchived",
"chat",
@@ -204,6 +204,7 @@ const CHAT_COMMANDS: [&str; 34] = [
"protect",
"unprotect",
"delchat",
"export-chat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
"listmsgs",

View File

@@ -251,16 +251,7 @@ class DirectImap:
return res
def append(self, folder, msg):
"""Upload a message to *folder*.
Trailing whitespace or a linebreak at the beginning will be removed automatically.
"""
if msg.startswith("\n"):
msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
self.conn.append(folder, msg)
def get_uid_by_message_id(self, message_id):
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]

View File

@@ -983,7 +983,7 @@ class TestOnlineAccount:
chat2 = msg2.chat
assert msg2 in chat2.get_messages()
assert chat2.is_deaddrop()
assert chat2.count_fresh_messages() == 1
assert chat2.count_fresh_messages() == 0
assert msg2.time_received >= msg1.time_sent
lp.sec("create new chat with contact and verify it's proper")
@@ -1319,11 +1319,9 @@ class TestOnlineAccount:
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
def test_dont_show_emails_in_draft_folder(self, acfactory):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email."""
ac1 = acfactory.get_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.com").create_chat()
@@ -1344,7 +1342,7 @@ class TestOnlineAccount:
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts that is moved to Sent later
message in Drafts
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Sent", """
From: ac1 <{}>
@@ -1357,7 +1355,6 @@ class TestOnlineAccount:
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
ac1.start_io()
msg = ac1._evtracker.wait_next_messages_changed()
@@ -1368,18 +1365,6 @@ class TestOnlineAccount:
assert msg.text == "subj message in Sent"
assert len(msg.chat.get_messages()) == 1
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1.direct_imap.conn.move(uid, "Sent")
ac1.start_io()
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 2
def test_prefer_encrypt(self, acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)

63
spec.md
View File

@@ -1,6 +1,6 @@
# chat-mail specification
Version: 0.33.0
Version: 0.32.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -301,9 +301,9 @@ to add a `Chat-Group-Avatar` only on image changes.
A user MAY have a profile-image that MAY be distributed to their contacts.
To change or set the profile-image,
the messenger MUST add the header `Chat-User-Avatar: base64:IMAGEDATA`.
To bypass limits of headers, it is recommended not to use the outer header
and to limit the size to 20k.
the messenger MUST attach an image file to a message
and MUST add the header `Chat-User-Avatar`
with the value set to the image name.
To remove the profile-image,
the messenger MUST add the header `Chat-User-Avatar: 0`.
@@ -320,14 +320,19 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-User-Avatar: photo.jpg
Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
Chat-User-Avatar: base64:AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQY ...
Hello, I've changed my profile image.
--==break==
Content-Type: image/jpeg
Content-Disposition: attachment; filename="photo.jpg"
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
--==break==--
The image format SHOULD be image/jpeg or image/png.
@@ -337,11 +342,6 @@ in the same message.
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
only on image changes.
In older specs, the profile-image was sent as an attachment
and `Chat-User-Avatar:` specified its name.
However, it turned out that these attachements are kind of unuexpected to users,
therefore the profile-image go to the header now.
# Locations
@@ -401,41 +401,9 @@ it is fine if the location is detected on forwarding etc.
</kml>
# Stickers
# Miscellaneous
Stickers are send as normal images
with the additional header `Chat-Content: sticker`.
It is discouraged to send stickers together with user generated text,
however, stickers can be used as a reply to a message
and also the footer should be set as usual.
From: alice@example.org
To: bob@example.com
Chat-Version: 1.0
Chat-Content: sticker
Message-ID: Mr.12345uvwxyZ.0005@example.org
Subject: Message from Alice
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
--
Hi there! I am using this new messenger!
--==break==
Content-Type: image/png
Content-Disposition: attachment; filename="sticker.png"
R0lGODlhpAGkAfe9AP+zd2eQkZhrI//z9v++PMb///+scrdDT3BtbtrZ2f/LQSsREcdIVf9 ...
--==break==--
Typical sticker formats are `image/png`, `image/gif` and `image/webp`.
Animated stickers are supported
by just using an image format that supports animation.
# Voice messages
Messengers SHOULD use the header `In-Reply-To` as usual.
Messengers SHOULD add a `Chat-Voice-message: 1` header
if an attached audio file is a voice message.
@@ -449,11 +417,6 @@ This allows the receiver to show the time without knowing the file format.
Chat-Voice-Message: 1
Chat-Duration: 10000
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
Messengers MAY send and receive Message Disposition Notifications
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
[RFC 3503](https://tools.ietf.org/html/rfc3503))
@@ -474,4 +437,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © 2017-2021 Delta Chat contributors.
Copyright © 2017-2020 Delta Chat contributors.

View File

@@ -452,7 +452,7 @@ impl<'a> BlobObject<'a> {
img.write_to(encoded, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
fn encode_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
max_bytes: Option<usize>,
@@ -477,7 +477,7 @@ impl<'a> BlobObject<'a> {
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
exceeds_width || encode_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
if do_scale || do_rotate {
@@ -500,7 +500,7 @@ impl<'a> BlobObject<'a> {
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if encode_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
@@ -511,10 +511,6 @@ impl<'a> BlobObject<'a> {
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px)",
@@ -621,8 +617,7 @@ mod tests {
use super::*;
use crate::{message::Message, test_utils::TestContext};
use image::Pixel;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_create() {
@@ -920,148 +915,4 @@ mod tests {
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_recode_image() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
1000,
1000,
0,
WORSE_IMAGE_SIZE,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
Some("0"),
bytes,
2000,
1800,
270,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let mut bytes = vec![];
img_rotated
.write_to(&mut bytes, image::ImageFormat::Jpeg)
.unwrap();
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let img_rotated = send_image_check_mediaquality(
Some("1"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
WORSE_IMAGE_SIZE * 1800 / 2000,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
}
fn assert_correct_rotation(img: &DynamicImage) {
// The test images are black in the bottom left corner after correctly applying
// the EXIF orientation
let [luma] = img.get_pixel(10, 10).to_luma().0;
assert_eq!(luma, 255);
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
assert_eq!(luma, 255);
let [luma] = img
.get_pixel(img.width() - 10, img.height() - 10)
.to_luma()
.0;
assert_eq!(luma, 255);
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
assert_eq!(luma, 0);
}
async fn send_image_check_mediaquality(
media_quality_config: Option<&str>,
bytes: &[u8],
original_width: u32,
original_height: u32,
orientation: i32,
compressed_width: u32,
compressed_height: u32,
) -> anyhow::Result<DynamicImage> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
let file = alice.get_blobdir().join("file.jpg");
File::create(&file).await?.write_all(bytes).await?;
let img = image::open(&file)?;
assert_eq!(img.width(), original_width);
assert_eq!(img.height(), original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
bob.recv_msg(&sent).await;
let bob_msg = bob.get_last_msg().await;
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
let img = image::open(file)?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
Ok(img)
}
}

View File

@@ -574,26 +574,10 @@ impl ChatId {
/// Returns number of messages in a chat.
pub async fn get_msg_cnt(self, context: &Context) -> Result<usize> {
let count = if self.is_deaddrop() {
context
.sql
.count(
"SELECT COUNT(*)
FROM msgs
WHERE hidden=0
AND chat_id IN (SELECT id FROM chats WHERE blocked=?)",
paramsv![Blocked::Deaddrop],
)
.await?
} else {
context
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
paramsv![self],
)
.await?
};
let count = context
.sql
.count("SELECT COUNT(*) FROM msgs WHERE chat_id=?", paramsv![self])
.await?;
Ok(count as usize)
}
@@ -608,31 +592,17 @@ impl ChatId {
// the times are average, no matter if there are fresh messages or not -
// and have to be multiplied by the number of items shown at once on the chatlist,
// so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :)
let count = if self.is_deaddrop() {
context
.sql
.count(
"SELECT COUNT(*)
FROM msgs
WHERE state=?
AND hidden=0
AND chat_id IN (SELECT id FROM chats WHERE blocked=?)",
paramsv![MessageState::InFresh, Blocked::Deaddrop],
)
.await?
} else {
context
.sql
.count(
"SELECT COUNT(*)
let count = context
.sql
.count(
"SELECT COUNT(*)
FROM msgs
WHERE state=?
WHERE state=10
AND hidden=0
AND chat_id=?;",
paramsv![MessageState::InFresh, self],
)
.await?
};
paramsv![self],
)
.await?;
Ok(count as usize)
}
@@ -3096,8 +3066,6 @@ mod tests {
use crate::contact::Contact;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use async_std::fs::File;
use async_std::prelude::*;
#[async_std::test]
async fn test_chat_info() {
@@ -4035,8 +4003,6 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), DC_CHAT_ID_DEADDROP);
assert_eq!(DC_CHAT_ID_DEADDROP.get_msg_cnt(&t).await?, 1);
assert_eq!(DC_CHAT_ID_DEADDROP.get_fresh_msg_cnt(&t).await?, 1);
let msgs = get_chat_msgs(&t, DC_CHAT_ID_DEADDROP, 0, None).await?;
assert_eq!(msgs.len(), 1);
let msg_id = match msgs.first().unwrap() {
@@ -4057,65 +4023,4 @@ mod tests {
Ok(())
}
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
let file = alice.get_blobdir().join(filename);
File::create(&file).await?.write_all(bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let mime = sent_msg.payload();
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
bob.recv_msg(&sent_msg).await;
let msg = bob.get_last_msg().await;
assert_eq!(msg.chat_id, bob_chat.id);
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
assert_eq!(msg.get_filename(), Some(filename.to_string()));
assert_eq!(msg.get_width(), w);
assert_eq!(msg.get_height(), h);
assert!(msg.get_filebytes(&bob).await > 250);
Ok(())
}
#[async_std::test]
async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
include_bytes!("../test-data/image/avatar64x64.png"),
64,
64,
)
.await
}
#[async_std::test]
async fn test_sticker_jpeg() -> Result<()> {
test_sticker(
"sticker.jpg",
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
1000,
1000,
)
.await
}
#[async_std::test]
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
include_bytes!("../test-data/image/image100x50.gif"),
100,
50,
)
.await
}
}

View File

@@ -18,18 +18,7 @@ use crate::stock_str;
/// The available configuration keys.
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Display,
EnumString,
AsRefStr,
EnumIter,
EnumProperty,
PartialOrd,
Ord,
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr, EnumIter, EnumProperty,
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {

View File

@@ -345,8 +345,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 600);
// Configure IMAP
let (_s, r) = async_std::channel::bounded(1);
let mut imap = Imap::new(r);
let mut imap: Option<Imap> = None;
let mut imap_configured = false;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
@@ -359,9 +361,18 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
match try_imap_one_param(ctx, &param.imap, &param.addr, oauth2, provider_strict_tls).await {
Ok(configured_imap) => {
imap = Some(configured_imap);
match try_imap_one_param(
ctx,
&param.imap,
&param.addr,
oauth2,
provider_strict_tls,
&mut imap,
)
.await
{
Ok(_) => {
imap_configured = true;
break;
}
Err(e) => errors.push(e),
@@ -371,10 +382,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let mut imap = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
progress!(ctx, 850);
@@ -510,38 +520,26 @@ async fn try_imap_one_param(
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
imap: &mut Imap,
) -> Result<(), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
let (_s, r) = async_std::channel::bounded(1);
let mut imap = match Imap::new(param, addr, oauth2, provider_strict_tls, r).await {
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {
config: inf,
msg: err.to_string(),
});
}
Ok(imap) => imap,
};
match imap.connect(context).await {
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {
config: inf,
msg: err.to_string(),
});
}
Ok(()) => {
info!(context, "success: {}", inf);
return Ok(imap);
}
if let Err(err) = imap
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
Ok(())
}
}

View File

@@ -236,13 +236,13 @@ impl Contact {
}
/// Block the given contact.
pub async fn block(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, true).await
pub async fn block(context: &Context, id: u32) {
set_block_contact(context, id, true).await;
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, false).await
pub async fn unblock(context: &Context, id: u32) {
set_block_contact(context, id, false).await;
}
/// Add a single contact as a result of an _explicit_ user action.
@@ -270,7 +270,7 @@ impl Contact {
}
}
if blocked {
Contact::unblock(context, contact_id).await?;
Contact::unblock(context, contact_id).await;
}
Ok(contact_id)
@@ -860,7 +860,7 @@ impl Contact {
"Can not delete special contact"
);
let count_chats = context
let count_contacts = context
.sql
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
@@ -868,7 +868,19 @@ impl Contact {
)
.await?;
if count_chats == 0 {
let count_msgs = if count_contacts > 0 {
context
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
)
.await?
} else {
0
};
if count_msgs == 0 {
match context
.sql
.execute(
@@ -890,9 +902,9 @@ impl Contact {
info!(
context,
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
);
bail!("Could not delete contact with ongoing chats");
bail!("Could not delete contact with messages in it");
}
/// Get a single contact object. For a list, see eg. dc_get_contacts().
@@ -1162,58 +1174,56 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
}
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can't block special contact {}",
contact_id
);
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return;
}
let contact = Contact::load_from_db(context, contact_id).await?;
if contact.blocked != new_blocking {
context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
)
.await?;
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context
.sql
.execute(
r#"
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
if contact.blocked != new_blocking
&& context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
)
.await
.is_ok()
{
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context
.sql
.execute(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
paramsv![new_blocking, Chattype::Single, contact_id],
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
paramsv![new_blocking, Chattype::Single, contact_id],
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await {
chat_id.set_blocked(context, Blocked::Not).await;
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await
{
chat_id.set_blocked(context, Blocked::Not).await;
}
}
}
}
Ok(())
}
/// Set profile image for a contact.
@@ -1608,34 +1618,6 @@ mod tests {
assert!(!contact.is_blocked());
}
#[async_std::test]
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(Contact::delete(&alice, DC_CONTACT_ID_SELF).await.is_err());
// Create Bob contact
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await
.unwrap();
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
// Can't delete a contact with ongoing chats.
assert!(Contact::delete(&alice, contact_id).await.is_err());
// Delete chat.
chat.get_id().delete(&alice).await?;
// Can delete contact now.
Contact::delete(&alice, contact_id).await?;
Ok(())
}
#[async_std::test]
async fn test_remote_authnames() {
let t = TestContext::new().await;

View File

@@ -161,9 +161,7 @@ impl Context {
{
let l = &mut *self.inner.scheduler.write().await;
if let Err(err) = l.start(self.clone()).await {
error!(self, "Failed to start IO: {}", err)
}
l.start(self.clone()).await;
}
}
@@ -540,16 +538,19 @@ impl Context {
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox.as_deref() == Some(folder_name))
}
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox.as_deref() == Some(folder_name))
}
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
Ok(spam.as_deref() == Some(folder_name))
}

View File

@@ -685,26 +685,11 @@ async fn add_parts(
}
}
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
// then ignore the email.
//
// We only apply this heuristic to classical emails, as it is not reliable (some servers
// such as systemli.org in June 2021 remove their own Received headers on incoming mails)
// and we know Delta Chat never stores drafts on IMAP servers.
let is_draft = !context.is_sentbox(server_folder).await?
if !context.is_sentbox(server_folder).await?
&& mime_parser.get(HeaderDef::Received).is_none()
&& mime_parser.get(HeaderDef::ChatVersion).is_none();
// Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
// X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
// created by Thunderbird.
//
// This check is not necessary now, but may become useful if the `Received:` header check
// is removed completely later.
let is_draft = is_draft || mime_parser.get(HeaderDef::XMozillaDraftInfo).is_some();
if is_draft {
{
// Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them
// So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
info!(context, "Email is probably just a draft (TRASH)");
*chat_id = DC_CHAT_ID_TRASH;
allow_creation = false;
@@ -2757,14 +2742,11 @@ mod tests {
// Check that the ndn would be downloaded:
let headers = mailparse::parse_mail(raw_ndn).unwrap().headers;
assert!(crate::imap::prefetch_should_download(
&t,
&headers,
std::iter::empty(),
ShowEmails::Off
)
.await
.unwrap());
assert!(
crate::imap::prefetch_should_download(&t, &headers, ShowEmails::Off)
.await
.unwrap()
);
dc_receive_imf(&t, raw_ndn, "INBOX", 1, false)
.await
@@ -3033,9 +3015,7 @@ mod tests {
assert_eq!(msgs.len(), 0);
// Unblock contact and check if the next message arrives in real chat
Contact::unblock(&t, *blocked.first().unwrap())
.await
.unwrap();
Contact::unblock(&t, *blocked.first().unwrap()).await;
let blocked = Contact::get_all_blocked(&t).await.unwrap();
assert_eq!(blocked.len(), 0);

339
src/export_chat.rs Normal file
View File

@@ -0,0 +1,339 @@
//! Export chats module
//!
//! ## Export Format
//! The format of an exported chat is a zip file with the following structure:
//! ```text
//! ├── blobs/ # all files that are referenced by the chat
//! ├── msg_info/
//! │ └── [msg_id].txt # message info
//! ├── msg_source/
//! │ └── [msg_id].eml # email sourcecode of messages if availible¹
//! └── chat.json # chat info, messages and message authors
//! ```
//! ##### ¹ Saving Mime header
//! To save the mime header you need to have the config option [`SaveMimeHeaders`] enabled.
//! This option saves the mime headers on future messages. Normaly the original email source code is discarded to save space.
//! You can use the repl tool to do this job:
//! ```sh
//! $ cargo run --example repl --features=repl /path/to/account/db.sqlite
//! > set save_mime_headers 1
//! ```
//! [`SaveMimeHeaders`]: ../config/enum.Config.html#variant.SaveMimeHeaders
use crate::chat::*;
use crate::constants::Viewtype;
use crate::constants::DC_GCM_ADDDAYMARKER;
use crate::contact::*;
use crate::context::Context;
// use crate::error::Error;
use crate::dc_tools::time;
use crate::message::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use zip::write::FileOptions;
use crate::location::Location;
use serde::Serialize;
#[derive(Debug)]
struct ExportChatResult {
chat_json: String,
// locations_geo_json: String,
message_ids: Vec<MsgId>,
referenced_blobs: Vec<String>,
}
pub async fn export_chat_to_zip(context: &Context, chat_id: ChatId, filename: &str) {
let res = export_chat_data(&context, chat_id).await;
let destination = std::path::Path::new(filename);
let pack_res = pack_exported_chat(&context, res, destination).await;
match &pack_res {
Ok(()) => println!("Exported chat successfully to {}", filename),
Err(err) => println!("Error {:?}", err),
};
}
async fn pack_exported_chat(
context: &Context,
artifact: ExportChatResult,
destination: &Path,
) -> zip::result::ZipResult<()> {
let file = std::fs::File::create(&destination).unwrap();
let mut zip = zip::ZipWriter::new(file);
zip.start_file("chat.json", Default::default())?;
zip.write_all(artifact.chat_json.as_bytes())?;
zip.add_directory("blobs/", Default::default())?;
let options = FileOptions::default();
for blob_name in artifact.referenced_blobs {
let path = context.get_blobdir().join(&blob_name);
// println!("adding file {:?} as {:?} ...", path, &blob_name);
zip.start_file(format!("blobs/{}", &blob_name), options)?;
let mut f = File::open(path)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
zip.write_all(&*buffer)?;
buffer.clear();
}
zip.add_directory("msg_info/", Default::default())?;
zip.add_directory("msg_source/", Default::default())?;
for id in artifact.message_ids {
zip.start_file(format!("msg_info/{}.txt", id.to_u32()), options)?;
zip.write_all((get_msg_info(&context, id).await).as_bytes())?;
if let Some(mime_headers) = get_mime_headers(&context, id).await {
zip.start_file(format!("msg_source/{}.eml", id.to_u32()), options)?;
zip.write_all((mime_headers).as_bytes())?;
}
}
zip.finish()?;
Ok(())
}
#[derive(Serialize)]
struct ChatJSON {
chat_json_version: u8,
export_timestamp: i64,
name: String,
color: String,
profile_img: Option<String>,
contacts: HashMap<u32, ContactJSON>,
referenced_external_messages:Vec<ChatItemJSON>,
messages: Vec<ChatItemJSON>,
locations: Vec<Location>,
}
#[derive(Serialize)]
struct ContactJSON {
name: String,
email: String,
color: String,
profile_img: Option<String>,
}
#[derive(Serialize)]
struct FileReference {
name: String,
filesize: u64,
mime: String,
path: String,
}
#[derive(Serialize)]
struct Qoute {
quoted_text: String,
message_id: Option<u32>,
}
#[derive(Serialize)]
#[serde(tag = "type")]
enum ChatItemJSON {
Message {
id: u32,
author_id: u32, // from_id
view_type: Viewtype,
timestamp_sort: i64,
timestamp_sent: i64,
timestamp_rcvd: i64,
text: Option<String>,
attachment: Option<FileReference>,
location_id: Option<u32>,
is_info_message: bool,
show_padlock: bool,
state: MessageState,
is_forwarded: bool,
quote: Option<Qoute>
},
MessageError {
id: u32,
error: String,
},
DayMarker {
timestamp: i64,
},
}
impl ChatItemJSON {
pub async fn from_message(message: &Message, context: &Context) -> ChatItemJSON {
let msg_id = message.get_id();
ChatItemJSON::Message {
id: msg_id.to_u32(),
author_id: message.get_from_id(), // from_id
view_type: message.get_viewtype(),
timestamp_sort: message.timestamp_sort,
timestamp_sent: message.timestamp_sent,
timestamp_rcvd: message.timestamp_rcvd,
text: message.get_text(),
attachment: match message.get_file(context) {
Some(file) => Some(FileReference {
name: message.get_filename().unwrap_or_else(|| "".to_owned()),
filesize: message.get_filebytes(context).await,
mime: message.get_filemime().unwrap_or_else(|| "".to_owned()),
path: format!(
"blobs/{}",
file.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
),
}),
None => None,
},
location_id: match message.has_location() {
true => Some(message.location_id),
false => None,
},
is_info_message: message.is_info(),
show_padlock: message.get_showpadlock(),
state: message.get_state(),
is_forwarded: message.is_forwarded(),
quote: match message.quoted_text() {
Some(text) => match message.quoted_message(&context).await {
Ok(Some(msg)) => Some(Qoute {
quoted_text: text,
message_id: Some(msg.get_id().to_u32())
}),
Err(_) | Ok(None) => Some(Qoute {
quoted_text: text,
message_id: None
})
}
None => None
}
}
}
}
async fn export_chat_data(context: &Context, chat_id: ChatId) -> ExportChatResult {
let mut blobs = Vec::new();
let mut chat_author_ids = Vec::new();
// message_ids var is used for writing message info to files
let mut message_ids: Vec<MsgId> = Vec::new();
let mut message_json: Vec<ChatItemJSON> = Vec::new();
let mut referenced_external_messages: Vec<ChatItemJSON> = Vec::new();
for item in get_chat_msgs(context, chat_id, DC_GCM_ADDDAYMARKER, None).await {
if let Some(json_item) = match item {
ChatItem::Message { msg_id } => match Message::load_from_db(context, msg_id).await {
Ok(message) => {
let filename = message.get_filename();
if let Some(file) = filename {
// push referenced blobs (attachments)
blobs.push(file);
}
message_ids.push(message.id);
// populate contactid list
chat_author_ids.push(message.from_id);
if let Ok(Some(ex_msg)) = message.quoted_message(&context).await {
if ex_msg.get_chat_id() != chat_id {
// if external add it to the file
referenced_external_messages.push(ChatItemJSON::from_message(&ex_msg, &context).await)
// contacts don't need to be referenced, because these should only be private replies
}
}
Some(ChatItemJSON::from_message(&message, &context).await)
}
Err(error_message) => Some(ChatItemJSON::MessageError {
id: msg_id.to_u32(),
error: error_message.to_string(),
}),
},
ChatItem::DayMarker { timestamp } => Some(ChatItemJSON::DayMarker { timestamp }),
ChatItem::Marker1 => None,
} {
message_json.push(json_item)
}
}
// deduplicate contact list and load the contacts
chat_author_ids.sort();
chat_author_ids.dedup();
// load information about the authors
let mut chat_authors: HashMap<u32, ContactJSON> = HashMap::new();
chat_authors.insert(
0,
ContactJSON {
name: "Err: Contact not found".to_owned(),
email: "error@localhost".to_owned(),
profile_img: None,
color: "grey".to_owned(),
},
);
for author_id in chat_author_ids {
let contact = Contact::get_by_id(context, author_id).await;
if let Ok(c) = contact {
let profile_img_path: String;
if let Some(path) = c.get_profile_image(context).await {
profile_img_path = path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
// push referenced blobs (avatars)
blobs.push(profile_img_path.clone());
} else {
profile_img_path = "".to_owned();
}
chat_authors.insert(
author_id,
ContactJSON {
name: c.get_display_name().to_owned(),
email: c.get_addr().to_owned(),
profile_img: match profile_img_path != "" {
true => Some(profile_img_path),
false => None,
},
color: format!("{:#}", c.get_color()), // TODO
},
);
}
}
// Load information about the chat
let chat: Chat = Chat::load_from_db(context, chat_id).await.unwrap();
let chat_avatar = match chat.get_profile_image(context).await {
Some(img) => {
let path = img
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
blobs.push(path.clone());
Some(format!("blobs/{}", path))
}
None => None,
};
let chat_json = ChatJSON {
chat_json_version: 1,
export_timestamp: time(),
name: chat.get_name().to_owned(),
color: format!("{:#}", chat.get_color(&context).await),
profile_img: chat_avatar,
contacts: chat_authors,
referenced_external_messages,
messages: message_json,
locations: crate::location::get_range(&context, chat_id, 0, 0, crate::dc_tools::time())
.await,
};
blobs.sort();
blobs.dedup();
ExportChatResult {
chat_json: serde_json::to_string(&chat_json).unwrap(),
message_ids,
referenced_blobs: blobs,
}
}

View File

@@ -25,12 +25,6 @@ pub enum HeaderDef {
/// we need to check that header as well.
XMicrosoftOriginalMessageId,
/// Thunderbird header used to store Draft information.
///
/// Thunderbird 78.11.0 does not set \Draft flag on messages saved as "Template", but sets this
/// header, so it can be used to ignore such messages.
XMozillaDraftInfo,
ListId,
References,
InReplyTo,

View File

@@ -8,12 +8,13 @@ use std::{cmp, cmp::max, collections::BTreeMap};
use anyhow::{bail, format_err, Context as _, Result};
use async_imap::{
error::Result as ImapResult,
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
};
use async_std::channel::Receiver;
use async_std::prelude::*;
use num_traits::FromPrimitive;
use crate::chat;
use crate::config::Config;
use crate::constants::{
Chattype, ShowEmails, Viewtype, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
@@ -35,7 +36,6 @@ use crate::param::Params;
use crate::provider::Socket;
use crate::scheduler::InterruptInfo;
use crate::stock_str;
use crate::{chat, constants::DC_CONTACT_ID_SELF};
mod client;
mod idle;
@@ -92,10 +92,6 @@ pub struct Imap {
interrupt: Option<stop_token::StopSource>,
should_reconnect: bool,
login_failed_once: bool,
/// True if CAPABILITY command was run successfully once and config.can_* contain correct
/// values.
capabilities_determined: bool,
}
#[derive(Debug)]
@@ -115,27 +111,14 @@ impl async_imap::Authenticator for OAuth2 {
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq)]
enum FolderMeaning {
Unknown,
Spam,
Sent,
Drafts,
SentObjects,
Other,
}
impl FolderMeaning {
fn to_config(self) -> Option<Config> {
match self {
FolderMeaning::Unknown => None,
FolderMeaning::Spam => Some(Config::ConfiguredSpamFolder),
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
FolderMeaning::Drafts => None,
FolderMeaning::Other => None,
}
}
}
#[derive(Debug)]
struct ImapConfig {
pub addr: String,
@@ -145,7 +128,6 @@ struct ImapConfig {
pub selected_folder: Option<String>,
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
/// True if the server has MOVE capability as defined in
@@ -153,93 +135,58 @@ struct ImapConfig {
pub can_move: bool,
}
impl Imap {
/// Creates new disconnected IMAP client using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub async fn new(
lp: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
idle_interrupt: Receiver<InterruptInfo>,
) -> Result<Self> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
let strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let config = ImapConfig {
addr: addr.to_string(),
lp: lp.clone(),
strict_tls,
oauth2,
impl Default for ImapConfig {
fn default() -> Self {
ImapConfig {
addr: "".into(),
lp: Default::default(),
strict_tls: false,
oauth2: false,
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
can_move: false,
};
}
}
}
let imap = Imap {
impl Imap {
pub fn new(idle_interrupt: Receiver<InterruptInfo>) -> Self {
Imap {
idle_interrupt,
config,
session: None,
connected: false,
interrupt: None,
should_reconnect: false,
login_failed_once: false,
capabilities_determined: false,
};
Ok(imap)
config: Default::default(),
session: Default::default(),
connected: Default::default(),
interrupt: Default::default(),
should_reconnect: Default::default(),
login_failed_once: Default::default(),
}
}
/// Creates new disconnected IMAP client using configured parameters.
pub async fn new_configured(
context: &Context,
idle_interrupt: Receiver<InterruptInfo>,
) -> Result<Self> {
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
pub fn is_connected(&self) -> bool {
self.connected
}
let param = LoginParam::from_database(context, "configured_").await?;
// the trailing underscore is correct
pub fn should_reconnect(&self) -> bool {
self.should_reconnect
}
let imap = Self::new(
&param.imap,
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
param.provider.map_or(false, |provider| provider.strict_tls),
idle_interrupt,
)
.await?;
Ok(imap)
pub fn trigger_reconnect(&mut self) {
self.should_reconnect = true;
}
/// Connects or reconnects if needed.
///
/// It is safe to call this function if already connected, actions are performed only as needed.
///
/// Does not emit network errors, can be used to try various parameters during
/// autoconfiguration.
///
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub async fn connect(&mut self, context: &Context) -> Result<()> {
/// It is safe to call this function if already connected, actions
/// are performed only as needed.
async fn try_setup_handle(&mut self, context: &Context) -> Result<()> {
if self.config.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down");
}
if self.should_reconnect() {
self.disconnect(context).await;
self.unsetup_handle(context).await;
self.should_reconnect = false;
} else if self.is_connected() {
return Ok(());
@@ -309,10 +256,6 @@ impl Imap {
self.connected = true;
self.session = Some(session);
self.login_failed_once = false;
emit_event!(
context,
EventType::ImapConnected(format!("IMAP-LOGIN as {}", self.config.lp.user))
);
Ok(())
}
@@ -349,49 +292,20 @@ impl Imap {
}
}
/// Determine server capabilities if not done yet.
async fn determine_capabilities(&mut self) -> Result<()> {
if self.capabilities_determined {
return Ok(());
}
match &mut self.session {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
self.config.can_idle = caps.has_str("IDLE");
self.config.can_move = caps.has_str("MOVE");
self.capabilities_determined = true;
Ok(())
}
Err(err) => {
bail!("CAPABILITY command error: {}", err);
}
},
None => {
bail!("Can't determine server capabilities because connection was not established")
}
}
}
/// Prepare for IMAP operation.
/// Connects or reconnects if not already connected.
///
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
///
/// This function emits network error if it fails. It should not be used during configuration
/// to avoid showing failed attempt errors to the user.
pub async fn prepare(&mut self, context: &Context) -> Result<()> {
let res = self.connect(context).await;
/// This function emits network error if it fails. It should not
/// be used during configuration to avoid showing failed attempt
/// errors to the user.
async fn setup_handle(&mut self, context: &Context) -> Result<()> {
let res = self.try_setup_handle(context).await;
if let Err(ref err) = res {
emit_event!(context, EventType::ErrorNetwork(err.to_string()));
}
self.ensure_configured_folders(context, true).await?;
self.determine_capabilities().await?;
res
}
async fn disconnect(&mut self, context: &Context) {
async fn unsetup_handle(&mut self, context: &Context) {
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
@@ -404,21 +318,139 @@ impl Imap {
}
}
self.connected = false;
self.capabilities_determined = false;
self.config.selected_folder = None;
self.config.selected_mailbox = None;
}
pub fn is_connected(&self) -> bool {
self.connected
async fn free_connect_params(&mut self) {
let mut cfg = &mut self.config;
cfg.addr = "".into();
cfg.lp = Default::default();
cfg.can_idle = false;
cfg.can_move = false;
}
pub fn should_reconnect(&self) -> bool {
self.should_reconnect
/// Connects to IMAP account using already-configured parameters.
///
/// Emits network error if connection fails.
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
if self.is_connected() && !self.should_reconnect() {
return Ok(());
}
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::from_database(context, "configured_").await?;
// the trailing underscore is correct
if let Err(err) = self
.connect(
context,
&param.imap,
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
param.provider.map_or(false, |provider| provider.strict_tls),
)
.await
{
bail!("IMAP Connection Failed with params {}: {}", param, err);
} else {
self.ensure_configured_folders(context, true).await
}
}
pub fn trigger_reconnect(&mut self) {
self.should_reconnect = true;
/// Tries connecting to imap account using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
///
/// Does not emit network errors, can be used to try various
/// parameters during autoconfiguration.
pub async fn connect(
&mut self,
context: &Context,
lp: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
) -> Result<()> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
{
let mut config = &mut self.config;
config.addr = addr.to_string();
config.lp = lp.clone();
config.strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
config.oauth2 = oauth2;
}
if let Err(err) = self.try_setup_handle(context).await {
warn!(context, "try_setup_handle: {}", err);
self.free_connect_params().await;
return Err(err);
}
let teardown = match &mut self.session {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open().await {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let can_move = caps.has_str("MOVE");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
} else {
s + &format!(" {:?}", c)
}
});
self.config.can_idle = can_idle;
self.config.can_move = can_move;
self.connected = true;
emit_event!(
context,
EventType::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.user, caps_list,
))
);
false
}
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
if teardown {
self.disconnect(context).await;
warn!(
context,
"IMAP disconnected immediately after connecting due to error"
);
}
Ok(())
}
pub async fn disconnect(&mut self, context: &Context) {
self.unsetup_handle(context).await;
self.free_connect_params().await;
}
pub async fn fetch(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
@@ -426,7 +458,7 @@ impl Imap {
// probably shutdown
bail!("IMAP operation attempted while it is torn down");
}
self.prepare(context).await?;
self.setup_handle(context).await?;
while self
.fetch_new_messages(context, &watch_folder, false)
@@ -664,7 +696,6 @@ impl Imap {
current_uid,
&headers,
&msg_id,
msg.flags(),
folder,
show_emails,
)
@@ -943,6 +974,10 @@ impl Imap {
(last_uid, read_errors)
}
pub async fn can_move(&self) -> bool {
self.config.can_move
}
pub async fn mv(
&mut self,
context: &Context,
@@ -967,7 +1002,7 @@ impl Imap {
let set = format!("{}", uid);
let display_folder_id = format!("{}/{}", folder, uid);
if self.config.can_move {
if self.can_move().await {
if let Some(ref mut session) = &mut self.session {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
@@ -1093,7 +1128,7 @@ impl Imap {
// TODO: make INBOX/SENT/MVBOX perform the jobs on their
// respective folders to avoid select_folder network traffic
// and the involved error states
if let Err(err) = self.prepare(context).await {
if let Err(err) = self.connect_configured(context).await {
warn!(context, "prepare_imap_op failed: {}", err);
return Some(ImapActionResult::RetryLater);
}
@@ -1265,8 +1300,9 @@ impl Imap {
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut sentbox_folder = None;
let mut spam_folder = None;
let mut mvbox_folder = None;
let mut folder_configs = BTreeMap::new();
let mut fallback_folder = get_fallback_folder(&delimiter);
while let Some(folder) = folders.next().await {
@@ -1285,26 +1321,31 @@ impl Imap {
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if folder.name() == "DeltaChat" {
// Always takes precedence
// Always takes precendent
mvbox_folder = Some(folder.name().to_string());
} else if folder.name() == fallback_folder {
// only set if none has been already set
// only set iff none has been already set
if mvbox_folder.is_none() {
mvbox_folder = Some(folder.name().to_string());
}
} else if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
} else if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
}
}
drop(folders);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
info!(context, "sentbox folder is {:?}", sentbox_folder);
if mvbox_folder.is_none() && create_mvbox {
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
@@ -1352,8 +1393,15 @@ impl Imap {
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
}
for (config, name) in folder_configs {
context.set_config(config, Some(&name)).await?;
if let Some(ref sentbox_folder) = sentbox_folder {
context
.set_config(Config::ConfiguredSentboxFolder, Some(sentbox_folder))
.await?;
}
if let Some(ref spam_folder) = spam_folder {
context
.set_config(Config::ConfiguredSpamFolder, Some(spam_folder))
.await?;
}
context
.sql
@@ -1426,50 +1474,29 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
"迷惑メール",
"스팸",
];
const DRAFT_NAMES: &[&str] = &[
"Drafts",
"Kladder",
"Entw?rfe",
"Borradores",
"Brouillons",
"Bozze",
"Concepten",
"Wersje robocze",
"Rascunhos",
"Entwürfe",
"Koncepty",
"Kopie robocze",
"Taslaklar",
"Utkast",
"Πρόχειρα",
"Черновики",
"下書き",
"草稿",
"임시보관함",
];
let lower = folder_name.to_lowercase();
if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Sent
FolderMeaning::SentObjects
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Spam
} else if DRAFT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Drafts
} else {
FolderMeaning::Unknown
}
}
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
let special_names = vec!["\\Trash", "\\Drafts"];
for attr in folder_name.attributes() {
if let NameAttribute::Custom(ref label) = attr {
match label.as_ref() {
"\\Trash" => return FolderMeaning::Other,
"\\Sent" => return FolderMeaning::Sent,
"\\Spam" | "\\Junk" => return FolderMeaning::Spam,
"\\Drafts" => return FolderMeaning::Drafts,
_ => {}
};
if special_names.iter().any(|s| *s == label) {
return FolderMeaning::Other;
} else if label == "\\Sent" {
return FolderMeaning::SentObjects;
} else if label == "\\Spam" || label == "\\Junk" {
return FolderMeaning::Spam;
}
}
}
FolderMeaning::Unknown
@@ -1587,7 +1614,6 @@ fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String>
pub(crate) async fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
mut flags: impl Iterator<Item = Flag<'_>>,
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
@@ -1625,17 +1651,12 @@ pub(crate) async fn prefetch_should_download(
.get_header_value(HeaderDef::AutocryptSetupMessage)
.is_some();
let (from_id, blocked_contact, origin) =
let (_contact_id, blocked_contact, origin) =
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
// (prevent_rename is the last argument of from_field_to_contact_id())
if flags.any(|f| f == Flag::Draft) && from_id == DC_CONTACT_ID_SELF {
info!(context, "Ignoring draft message");
return Ok(false);
}
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
@@ -1654,7 +1675,6 @@ async fn message_needs_processing(
current_uid: u32,
headers: &[mailparse::MailHeader<'_>],
msg_id: &str,
flags: impl Iterator<Item = Flag<'_>>,
folder: &str,
show_emails: ShowEmails,
) -> bool {
@@ -1678,7 +1698,7 @@ async fn message_needs_processing(
// we do not know the message-id
// or the message-id is missing (in this case, we create one in the further process)
// or some other error happened
let show = match prefetch_should_download(context, headers, flags, show_emails).await {
let show = match prefetch_should_download(context, headers, show_emails).await {
Ok(show) => show,
Err(err) => {
warn!(context, "prefetch_should_download error: {}", err);
@@ -1841,16 +1861,25 @@ mod tests {
use crate::test_utils::TestContext;
#[test]
fn test_get_folder_meaning_by_name() {
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
assert_eq!(
get_folder_meaning_by_name("Gesendet"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("GESENDET"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("gesendet"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("Messages envoyés"),
FolderMeaning::Sent
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
FolderMeaning::Sent
FolderMeaning::SentObjects
);
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);

View File

@@ -25,7 +25,7 @@ impl Imap {
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
self.prepare(context).await?;
self.setup_handle(context).await?;
self.select_folder(context, watch_folder.as_deref()).await?;
@@ -158,7 +158,7 @@ impl Imap {
// 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.prepare(context).await {
if let Err(err) = self.connect_configured(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}

View File

@@ -1,13 +1,13 @@
use std::{collections::BTreeMap, time::Instant};
use std::time::Instant;
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::context::Context;
use crate::imap::Imap;
use crate::{config::Config, log::LogExt};
use crate::{context::Context, imap::FolderMeaning};
use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name};
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
impl Imap {
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
@@ -25,13 +25,14 @@ impl Imap {
}
info!(context, "Starting full folder scan");
self.prepare(context).await?;
self.connect_configured(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let watched_folders = get_watched_folders(context).await;
let mut folder_configs = BTreeMap::new();
let mut sentbox_folder = None;
let mut spam_folder = None;
for folder in folders {
let folder = match folder {
@@ -42,41 +43,38 @@ impl Imap {
}
};
let foldername = folder.name();
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
let folder_name_meaning = get_folder_meaning_by_name(foldername);
if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
}
let is_drafts = folder_meaning == FolderMeaning::Drafts
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Drafts);
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
self.fetch_new_messages(context, folder.name(), false)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
if !watched_folders.contains(&foldername.to_string()) {
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
}
}
}
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
// `ConfiguredSentboxFolder` is set to `None`:
for config in &[
Config::ConfiguredSentboxFolder,
Config::ConfiguredSpamFolder,
] {
context
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
.await?;
}
context
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
.await?;
context
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
.await?;
last_scan.replace(Instant::now());
Ok(())

View File

@@ -612,13 +612,8 @@ async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result
let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
}
// Load a single blob into memory
match context
let (file_name, file_blob) = context
.sql
.query_row(
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
@@ -629,17 +624,12 @@ async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result
Ok((file_name, file_blob))
},
)
.await
{
Ok((file_name, file_blob)) => {
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
}
Err(err) => {
error!(context, "Can't import file {}: {}", file_id, err);
}
}
.await?;
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
}
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
@@ -648,6 +638,12 @@ async fn import_backup_old(context: &Context, backup_to_import: &Path) -> Result
permille = 990
}
context.emit_event(EventType::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
}
if all_files_extracted {

View File

@@ -532,7 +532,7 @@ impl Job {
}
async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -594,7 +594,7 @@ impl Job {
/// records pointing to the same message on the server, the job
/// also removes the message on the server.
async fn delete_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -682,7 +682,7 @@ impl Job {
if job_try!(context.get_config_bool(Config::Bot).await) {
return Status::Finished(Ok(())); // Bots don't want those messages
}
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -755,7 +755,7 @@ impl Job {
/// Chat in contrast to the Sent folder, which is normally managed
/// by the user via webmail or another email client.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -779,7 +779,7 @@ impl Job {
}
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}

View File

@@ -51,6 +51,7 @@ pub mod contact;
pub mod context;
mod e2ee;
pub mod ephemeral;
pub mod export_chat;
mod imap;
pub mod imex;
mod scheduler;

View File

@@ -16,9 +16,8 @@ use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Params;
use crate::stock_str;
/// Location record
#[derive(Debug, Clone, Default)]
use serde::Serialize;
#[derive(Debug, Clone, Default, Serialize)]
pub struct Location {
pub location_id: u32,
pub latitude: f64,

View File

@@ -27,7 +27,7 @@ use crate::events::EventType;
use crate::job::{self, Action};
use crate::log::LogExt;
use crate::lot::{Lot, LotState, Meaning};
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::mimeparser::{FailureReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::stock_str;
@@ -400,9 +400,7 @@ impl Message {
let msg = Message {
id: row.get("id")?,
rfc724_mid: row.get::<_, String>("rfc724mid")?,
in_reply_to: row
.get::<_, Option<String>>("mime_in_reply_to")?
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
in_reply_to: row.get::<_, Option<String>>("mime_in_reply_to")?,
server_folder: row.get::<_, Option<String>>("server_folder")?,
server_uid: row.get("server_uid")?,
chat_id: row.get("chat_id")?,
@@ -1196,11 +1194,7 @@ pub async fn decide_on_contact_request(
Err(e) => warn!(context, "decide_on_contact_request error: {}", e),
},
(Block, false) => {
if let Err(e) = Contact::block(context, msg.from_id).await {
warn!(context, "Can't block contact: {}", e);
}
}
(Block, false) => Contact::block(context, msg.from_id).await,
(Block, true) => {
if !msg.chat_id.set_blocked(context, Blocked::Manually).await {
warn!(context, "Block mailing list failed.")

View File

@@ -398,7 +398,7 @@ impl MimeMessage {
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
part.typ = Viewtype::Voice;
}
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
if part.typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
@@ -2955,46 +2955,4 @@ On 2020-10-25, Bob wrote:
Some("Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com".to_string())
);
}
#[async_std::test]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;
// A message with a long Message-ID.
// Long message-IDs are generated by Mailjet.
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
To: Bob <bob@example.org>
From: Alice <alice@example.org>
Subject: ...
Some quote.
"###;
dc_receive_imf(&t, raw, "INBOX", 1, false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To:
<ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <foobar@example.org>
To: Alice <alice@example.org>
From: Bob <bob@example.org>
Subject: ...
> Some quote.
Some reply
"###;
dc_receive_imf(&t, raw, "INBOX", 2, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply");
let quoted_message = msg.quoted_message(&t).await?.unwrap();
assert_eq!(quoted_message.get_text().unwrap(), "Some quote.");
Ok(())
}
}

View File

@@ -876,35 +876,6 @@ static P_ROGERS_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// systemausfall.org.md: systemausfall.org, solidaris.me
static P_SYSTEMAUSFALL_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "systemausfall.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/systemausfall-org",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "mail.systemausfall.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.systemausfall.org",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// systemli.org.md: systemli.org
static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "systemli.org",
@@ -1020,23 +991,6 @@ static P_TISCALI_IT: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// tutanota.md: tutanota.com, tutanota.de, tutamail.com, tuta.io, keemail.me
static P_TUTANOTA: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "tutanota",
status: Status::Broken,
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Tutanota.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/tutanota",
server: vec![
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// ukr.net.md: ukr.net
static P_UKR_NET: Lazy<Provider> = Lazy::new(|| Provider {
id: "ukr.net",
@@ -1331,18 +1285,11 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("protonmail.ch", &*P_PROTONMAIL),
("riseup.net", &*P_RISEUP_NET),
("rogers.com", &*P_ROGERS_COM),
("systemausfall.org", &*P_SYSTEMAUSFALL_ORG),
("solidaris.me", &*P_SYSTEMAUSFALL_ORG),
("systemli.org", &*P_SYSTEMLI_ORG),
("t-online.de", &*P_T_ONLINE),
("magenta.de", &*P_T_ONLINE),
("testrun.org", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("tutanota.com", &*P_TUTANOTA),
("tutanota.de", &*P_TUTANOTA),
("tutamail.com", &*P_TUTANOTA),
("tuta.io", &*P_TUTANOTA),
("keemail.me", &*P_TUTANOTA),
("ukr.net", &*P_UKR_NET),
("undernet.uy", &*P_UNDERNET_UY),
("vfemail.net", &*P_VFEMAIL),
@@ -1442,12 +1389,10 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("protonmail", &*P_PROTONMAIL),
("riseup.net", &*P_RISEUP_NET),
("rogers.com", &*P_ROGERS_COM),
("systemausfall.org", &*P_SYSTEMAUSFALL_ORG),
("systemli.org", &*P_SYSTEMLI_ORG),
("t-online", &*P_T_ONLINE),
("testrun", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("tutanota", &*P_TUTANOTA),
("ukr.net", &*P_UKR_NET),
("undernet.uy", &*P_UNDERNET_UY),
("vfemail", &*P_VFEMAIL),
@@ -1463,4 +1408,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 6, 7));
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 4, 10));

View File

@@ -1,4 +1,3 @@
use anyhow::{bail, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
@@ -131,7 +130,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Ok(Some(watch_folder)) => {
if let Err(err) = connection.prepare(ctx).await {
if let Err(err) = connection.connect_configured(ctx).await {
error_network!(ctx, "{}", err);
return;
}
@@ -160,7 +159,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
match ctx.get_config(folder).await {
Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.prepare(ctx).await {
if let Err(err) = connection.connect_configured(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
@@ -302,11 +301,11 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
impl Scheduler {
/// Start the scheduler, panics if it is already running.
pub async fn start(&mut self, ctx: Context) -> Result<()> {
let (mvbox, mvbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (sentbox, sentbox_handlers) = ImapConnectionState::new(&ctx).await?;
pub async fn start(&mut self, ctx: Context) {
let (mvbox, mvbox_handlers) = ImapConnectionState::new();
let (sentbox, sentbox_handlers) = ImapConnectionState::new();
let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (inbox, inbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (inbox, inbox_handlers) = ImapConnectionState::new();
let (inbox_start_send, inbox_start_recv) = channel::bounded(1);
let (mvbox_start_send, mvbox_start_recv) = channel::bounded(1);
@@ -322,7 +321,11 @@ impl Scheduler {
}))
};
if ctx.get_config_bool(Config::MvboxWatch).await? {
if ctx
.get_config_bool(Config::MvboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -340,7 +343,11 @@ impl Scheduler {
.expect("mvbox start send, missing receiver");
}
if ctx.get_config_bool(Config::SentboxWatch).await? {
if ctx
.get_config_bool(Config::SentboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -384,11 +391,10 @@ impl Scheduler {
.try_join(smtp_start_recv.recv())
.await
{
bail!("failed to start scheduler: {}", err);
error!(ctx, "failed to start scheduler: {}", err);
}
info!(ctx, "scheduler is running");
Ok(())
}
async fn maybe_network(&self) {
@@ -582,13 +588,13 @@ pub(crate) struct ImapConnectionState {
impl ImapConnectionState {
/// Construct a new connection.
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
fn new() -> (Self, ImapConnectionHandlers) {
let (stop_sender, stop_receiver) = channel::bounded(1);
let (shutdown_sender, shutdown_receiver) = channel::bounded(1);
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
let handlers = ImapConnectionHandlers {
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
connection: Imap::new(idle_interrupt_receiver),
stop_receiver,
shutdown_sender,
};
@@ -601,7 +607,7 @@ impl ImapConnectionState {
let conn = ImapConnectionState { state };
Ok((conn, handlers))
(conn, handlers)
}
/// Interrupt any form of idle.

View File

@@ -253,17 +253,17 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
#[derive(Debug, thiserror::Error)]
pub enum JoinError {
#[error("Unknown QR-code: {0}")]
#[error("Unknown QR-code")]
QrCode(#[from] QrError),
#[error("A setup-contact/secure-join protocol is already running")]
AlreadyRunning,
#[error("An \"ongoing\" process is already running")]
OngoingRunning,
#[error("Failed to send handshake message: {0}")]
#[error("Failed to send handshake message")]
SendMessage(#[from] SendMsgError),
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
// is supposed to create a contact for us.
#[error("Unknown contact (this is a bug): {0}")]
#[error("Unknown contact (this is a bug)")]
UnknownContact(#[source] anyhow::Error),
// Note that this can only occur if we failed to create the chat correctly.
#[error("Ongoing sender dropped (this is a bug)")]

View File

@@ -198,7 +198,7 @@ impl Sql {
}
}
info!(context, "Opened database {:?}.", dbfile);
info!(context, "Opened {:?}.", dbfile);
Ok(())
}
@@ -641,12 +641,8 @@ async fn maybe_add_from_param(
paramsv![],
|row| row.get::<_, String>(0),
|rows| {
// Rows that can't be parsed, for example if they are
// not UTF-8 strings, are ignored. It is possible
// when upgrading from C core to Rust core, which
// guarantees UTF-8 strings everywhere.
for row in rows.filter_map(|row| row.ok()) {
let param: Params = row.parse().unwrap_or_default();
for row in rows {
let param: Params = row?.parse().unwrap_or_default();
if let Some(file) = param.get(param_id) {
maybe_add_file(files_in_use, file);
}
@@ -806,36 +802,4 @@ mod test {
Ok(())
}
#[async_std::test]
async fn test_migration_flags() -> Result<()> {
let t = TestContext::new().await;
t.evtracker.get_info_contains("Opened database").await;
// as migrations::run() was already executed on context creation,
// another call should not result in any action needed.
// this test catches some bugs where dbversion was forgotten to be persisted.
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
migrations::run(&t, &t.sql).await?;
assert!(!recalc_fingerprints);
assert!(!update_icons);
assert!(!disable_server_delete);
assert!(!recode_avatar);
info!(&t, "test_migration_flags: XXX");
loop {
if let EventType::Info(info) = t.evtracker.recv().await.unwrap() {
assert!(
!info.contains("[migration]"),
"Migrations were run twice, you probably forgot to update the db version"
);
if info.contains("test_migration_flags: XXX") {
break;
}
}
}
Ok(())
}
}

View File

@@ -167,7 +167,7 @@ CREATE TABLE tokens (
ALTER TABLE acpeerstates ADD COLUMN verified_key;
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
39,
38,
)
.await?;
}
@@ -454,7 +454,7 @@ paramsv![]
info!(context, "[migration] v75");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
75,
74,
)
.await?;
}
@@ -466,7 +466,6 @@ paramsv![]
if dbversion < 77 {
info!(context, "[migration] v77");
recode_avatar = true;
sql.set_db_version(77).await?;
}
Ok((

View File

@@ -9,7 +9,6 @@ use std::{collections::BTreeMap, panic};
use std::{fmt, thread};
use ansi_term::Color;
use async_std::channel::Receiver;
use async_std::path::PathBuf;
use async_std::sync::{Arc, RwLock};
use async_std::{channel, pin::Pin};
@@ -48,7 +47,6 @@ static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
pub(crate) struct TestContext {
pub ctx: Context,
pub dir: TempDir,
pub evtracker: EvTracker,
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
recv_idx: RwLock<u32>,
/// Functions to call for events received.
@@ -105,8 +103,6 @@ impl TestContext {
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
let sinks = Arc::clone(&event_sinks);
let (poison_sender, poison_receiver) = channel::bounded(1);
let (evtracker_sender, evtracker_receiver) = channel::unbounded();
async_std::task::spawn(async move {
// Make sure that the test fails if there is a panic on this thread here:
let current_id = task::current().id();
@@ -126,15 +122,13 @@ impl TestContext {
sink(event.clone()).await;
}
}
receive_event(&event);
evtracker_sender.send(event.typ).await.ok();
receive_event(event);
}
});
Self {
ctx,
dir,
evtracker: EvTracker(evtracker_receiver),
recv_idx: RwLock::new(0),
event_sinks,
poison_receiver,
@@ -574,28 +568,6 @@ pub fn bob_keypair() -> key::KeyPair {
}
}
pub struct EvTracker(Receiver<EventType>);
impl EvTracker {
pub async fn get_info_contains(&self, s: &str) -> EventType {
loop {
let event = self.0.recv().await.unwrap();
if let EventType::Info(i) = &event {
if i.contains(s) {
return event;
}
}
}
}
}
impl Deref for EvTracker {
type Target = Receiver<EventType>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Gets a specific message from a chat and asserts that the chat has a specific length.
///
/// Panics if the length of the chat is not `asserted_msgs_count` or if the chat item at `index` is not a Message.
@@ -619,12 +591,12 @@ pub(crate) async fn get_chat_msg(
/// Pretty-print an event to stdout
///
/// Done during tests this is captured by `cargo test` and associated with the test itself.
fn receive_event(event: &Event) {
fn receive_event(event: Event) {
let green = Color::Green.normal();
let yellow = Color::Yellow.normal();
let red = Color::Red.normal();
let msg = match &event.typ {
let msg = match event.typ {
EventType::Info(msg) => format!("INFO: {}", msg),
EventType::SmtpConnected(msg) => format!("[SMTP_CONNECTED] {}", msg),
EventType::ImapConnected(msg) => format!("[IMAP_CONNECTED] {}", msg),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB