mirror of
https://github.com/chatmail/core.git
synced 2026-05-17 13:56:30 +03:00
Compare commits
70 Commits
py-1.60.0
...
bughunt_ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f57293607 | ||
|
|
b33ad05c3b | ||
|
|
398cea6466 | ||
|
|
1afd2f2d66 | ||
|
|
48f1ef3641 | ||
|
|
e95911a484 | ||
|
|
b1af486e10 | ||
|
|
bffb41326c | ||
|
|
c532055153 | ||
|
|
be595f8601 | ||
|
|
1d1d98e02b | ||
|
|
771e84af6e | ||
|
|
bbfed20d34 | ||
|
|
0f2095947c | ||
|
|
46956caf75 | ||
|
|
6f3dd7f0c2 | ||
|
|
15dcd62652 | ||
|
|
da2f30786b | ||
|
|
50a5e715d2 | ||
|
|
1bef623c89 | ||
|
|
7745db8310 | ||
|
|
1d1491c95d | ||
|
|
2a0f6f5cf7 | ||
|
|
b27793e852 | ||
|
|
8fb5e038a9 | ||
|
|
e518dc3331 | ||
|
|
ea1368a36b | ||
|
|
0aeb2bd6fb | ||
|
|
0263d0816a | ||
|
|
bb71f6ec98 | ||
|
|
02a1abc0d5 | ||
|
|
40fe65716f | ||
|
|
d05b399eac | ||
|
|
c31216f043 | ||
|
|
f66bde7275 | ||
|
|
7f819de49f | ||
|
|
5f065b245f | ||
|
|
3c43d790a3 | ||
|
|
d33177a721 | ||
|
|
aa2e03382b | ||
|
|
2a59e6121b | ||
|
|
1a438d61df | ||
|
|
444486f5df | ||
|
|
1eae2477c3 | ||
|
|
7b3eefc6c6 | ||
|
|
4dd0830baf | ||
|
|
8e3f062881 | ||
|
|
cf445f265a | ||
|
|
963c66b76c | ||
|
|
79df667e1e | ||
|
|
785c796bd6 | ||
|
|
6a2112ba66 | ||
|
|
3f170279da | ||
|
|
3408501a75 | ||
|
|
3b765cb3c9 | ||
|
|
8a9ea388ed | ||
|
|
77acf910bf | ||
|
|
c04c87658c | ||
|
|
fd784ec223 | ||
|
|
25f1b0c4af | ||
|
|
580ec6e6ce | ||
|
|
8e5195c4f6 | ||
|
|
729a1e1cd2 | ||
|
|
78b93f3621 | ||
|
|
4111489daf | ||
|
|
b7bd4c6ba7 | ||
|
|
83dc0bc2b0 | ||
|
|
1679ddddf0 | ||
|
|
de258645f4 | ||
|
|
b463b602a9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ deltachat-ffi/xml
|
||||
.rsynclist
|
||||
|
||||
coverage/
|
||||
.DS_Store
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,4 +1,16 @@
|
||||
# Changelog
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
- `Accounts` is not cloneable anymore #2654 #2658
|
||||
- always check certificates strictly when connecting over SOCKS5 in Automatic mode #2657
|
||||
- update chat/contact data only when there was no newer update #2642
|
||||
- improve Doxygen documentation style #2647
|
||||
|
||||
### Fixes
|
||||
- ignore MDNs sent to self #2674
|
||||
- fix pkg-config file #2660
|
||||
|
||||
## 1.60.0
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(deltachat LANGUAGES C)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
@@ -35,7 +36,6 @@ add_custom_target(
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
907
Cargo.lock
generated
907
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
100
Cargo.toml
100
Cargo.toml
@@ -15,76 +15,76 @@ lto = true
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1.0.42"
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
||||
async-native-tls = { version = "0.3.3" }
|
||||
async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20.3"
|
||||
async-std = { version = "~1.9.0", features = ["unstable"] }
|
||||
async-tar = "0.3.0"
|
||||
async-trait = "0.1.50"
|
||||
backtrace = "0.3.59"
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-tar = "0.3"
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.3.1"
|
||||
byteorder = "1.3.1"
|
||||
chrono = "0.4.6"
|
||||
dirs = { version = "3.0.2", optional=true }
|
||||
bitflags = "1.3"
|
||||
byteorder = "1.3"
|
||||
chrono = "0.4"
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
escaper = "0.1.1"
|
||||
futures = "0.3.16"
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
indexmap = "1.7.0"
|
||||
itertools = "0.10.1"
|
||||
indexmap = "1.7"
|
||||
itertools = "0.10"
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2.98"
|
||||
libc = "0.2"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
mailparse = "0.13.5"
|
||||
native-tls = "0.2.3"
|
||||
num_cpus = "1.13.0"
|
||||
num-derive = "0.3.0"
|
||||
num-traits = "0.2.6"
|
||||
mailparse = "0.13"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.8.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7.0", default-features = false }
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
quick-xml = "0.22.0"
|
||||
r2d2 = "0.8.9"
|
||||
r2d2_sqlite = "0.18.0"
|
||||
rand = "0.7.0"
|
||||
regex = "1.4.6"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.18"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = "0.25"
|
||||
rust-hsluv = "0.1.4"
|
||||
rustyline = { version = "8.2.0", optional = true }
|
||||
sanitize-filename = "0.3.0"
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "8.2", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.9.7"
|
||||
sha2 = "0.9.5"
|
||||
smallvec = "1.0.0"
|
||||
stop-token = "0.2.0"
|
||||
strum = "0.21.0"
|
||||
strum_macros = "0.21.1"
|
||||
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1.0.26"
|
||||
toml = "0.5.6"
|
||||
url = "2.2.2"
|
||||
sha-1 = "0.9"
|
||||
sha2 = "0.9"
|
||||
smallvec = "1"
|
||||
stop-token = "0.2"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
url = "2"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4.2"
|
||||
humansize = "1.1.1"
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
|
||||
async-std = { version = "1.10.0", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
futures-lite = "1.12.0"
|
||||
log = "0.4.11"
|
||||
pretty_assertions = "0.7.2"
|
||||
pretty_env_logger = "0.4.0"
|
||||
proptest = "1.0"
|
||||
tempfile = "3.0"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_assertions = "0.7"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = "1"
|
||||
tempfile = "3"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
||||
@@ -20,9 +20,9 @@ libc = "0.2"
|
||||
human-panic = "1.0.1"
|
||||
num-traits = "0.2.6"
|
||||
serde_json = "1.0"
|
||||
async-std = "1.9.0"
|
||||
anyhow = "1.0.42"
|
||||
thiserror = "1.0.26"
|
||||
async-std = "1.10.0"
|
||||
anyhow = "1.0.44"
|
||||
thiserror = "1.0.29"
|
||||
rand = "0.7.3"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -583,7 +583,7 @@ SORT_MEMBERS_CTORS_1ST = NO
|
||||
# appear in their defined order.
|
||||
# The default value is: NO.
|
||||
|
||||
SORT_GROUP_NAMES = NO
|
||||
SORT_GROUP_NAMES = YES
|
||||
|
||||
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
|
||||
# fully-qualified names, including namespaces. If set to NO, the class list will
|
||||
|
||||
@@ -4,4 +4,16 @@ div.fragment {
|
||||
background-color: #e0e0e0;
|
||||
border: 0;
|
||||
padding: 1em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #e0e0e0;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
@@ -351,6 +351,14 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
||||
* For larger messages, only the header is downloaded and a placeholder is shown.
|
||||
* These messages can be downloaded fully using dc_download_full_msg() later.
|
||||
* The limit is compared against raw message sizes, including headers.
|
||||
* The actually used limit may be corrected
|
||||
* to not mess up with non-delivery-reports or read-receipts.
|
||||
* 0=no limit (default).
|
||||
* Changes affect future messages only.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -586,6 +594,14 @@ void dc_configure (dc_context_t* context);
|
||||
* Typically, for unconfigured accounts, the user is prompted
|
||||
* to enter some settings and dc_configure() is called in a thread then.
|
||||
*
|
||||
* A once successfully configured context cannot become unconfigured again;
|
||||
* if a subsequent call to dc_configure() fails,
|
||||
* the prior configuration is used.
|
||||
*
|
||||
* However, of course, also a configuration may stop working,
|
||||
* as eg. the password was changed on the server.
|
||||
* To check that use eg. dc_get_connectivity().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @return 1=context is configured and can be used;
|
||||
@@ -907,7 +923,7 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
* dc_send_videochat_invitation() is blocking and may take a while,
|
||||
* so the UIs will typically call the function from within a thread.
|
||||
* Moreover, UIs will typically enter the room directly without an additional click on the message,
|
||||
* for this purpose, the function returns the message-id directly.
|
||||
* for this purpose, the function returns the message id directly.
|
||||
*
|
||||
* As for other messages sent, this function
|
||||
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
|
||||
@@ -1584,6 +1600,28 @@ char* dc_get_msg_info (dc_context_t* context, uint32_t ms
|
||||
char* dc_get_msg_html (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Asks the core to start downloading a message fully.
|
||||
* This function is typically called when the user hits the "Download" button
|
||||
* that is shown by the UI in case dc_msg_get_download_state()
|
||||
* returns @ref DC_DOWNLOAD_AVAILABLE or @ref DC_DOWNLOAD_FAILURE.
|
||||
*
|
||||
* On success, the @ref DC_MSG "view type of the message" may change
|
||||
* or the message may be replaced completely by one or more messages with other message ids.
|
||||
* That may happen eg. in cases where the message was encrypted
|
||||
* and the type could not be determined without fully downloading.
|
||||
* Downloaded content can be accessed as usual after download,
|
||||
* eg. using dc_msg_get_file().
|
||||
*
|
||||
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id Message ID to download the content for.
|
||||
*/
|
||||
void dc_download_full_msg (dc_context_t* context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Get the raw mime-headers of the given message.
|
||||
* Raw headers are saved for incoming messages
|
||||
@@ -1634,7 +1672,7 @@ void dc_forward_msgs (dc_context_t* context, const uint3
|
||||
*
|
||||
* - For normal chats, the IMAP state is updated, MDN is sent
|
||||
* (if dc_set_config()-options `mdns_enabled` is set)
|
||||
* and the internal state is changed to DC_STATE_IN_SEEN to reflect these actions.
|
||||
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
*
|
||||
* - For contact requests, no IMAP or MDNs is done
|
||||
* and the internal state is not changed therefore.
|
||||
@@ -2731,13 +2769,13 @@ uint32_t dc_array_get_contact_id (const dc_array_t* array, size_t in
|
||||
|
||||
|
||||
/**
|
||||
* Return the message-id of the item at the given index.
|
||||
* Return the message id of the item at the given index.
|
||||
*
|
||||
* @memberof dc_array_t
|
||||
* @param array The array object.
|
||||
* @param index Index of the item. Must be between 0 and dc_array_get_cnt()-1.
|
||||
* @return Message-id of the item at the given index.
|
||||
* 0 if there is no message-id bound to the given item,
|
||||
* @return Message id of the item at the given index.
|
||||
* 0 if there is no message id bound to the given item,
|
||||
*/
|
||||
uint32_t dc_array_get_msg_id (const dc_array_t* array, size_t index);
|
||||
|
||||
@@ -2883,7 +2921,7 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
|
||||
*
|
||||
* - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
|
||||
*
|
||||
* - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()). 0 if not applicable.
|
||||
* - dc_lot_t::state: The state of the message as one of the @ref DC_STATE constants. 0 if not applicable.
|
||||
*
|
||||
* @memberof dc_chatlist_t
|
||||
* @param chatlist The chatlist to query as returned e.g. from dc_get_chatlist().
|
||||
@@ -2899,7 +2937,7 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
|
||||
* Create a chatlist summary item when the chatlist object is already unref()'d.
|
||||
*
|
||||
* This function is similar to dc_chatlist_get_summary(), however,
|
||||
* takes the chat-id and message-id as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
|
||||
* takes the chat-id and message id as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
|
||||
* as arguments. The chatlist object itself is not needed directly.
|
||||
*
|
||||
* This maybe useful if you convert the complete object into a different represenation
|
||||
@@ -2937,7 +2975,7 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
|
||||
* color: color of this chat
|
||||
* last-message-from: who sent the last message
|
||||
* last-message-text: message (truncated)
|
||||
* last-message-state: DC_STATE* constant
|
||||
* last-message-state: @ref DC_STATE constant
|
||||
* last-message-date:
|
||||
* avatar-path: path-to-blobfile
|
||||
* is_verified: yes/no
|
||||
@@ -2961,12 +2999,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
|
||||
#define DC_CHAT_ID_LAST_SPECIAL 9 // larger chat IDs are "real" chats, their messages are "real" messages.
|
||||
|
||||
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
#define DC_CHAT_TYPE_SINGLE 100
|
||||
#define DC_CHAT_TYPE_GROUP 120
|
||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||
|
||||
|
||||
/**
|
||||
* Free a chat object.
|
||||
*
|
||||
@@ -2993,18 +3025,16 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get chat type.
|
||||
* Get chat type as one of the @ref DC_CHAT_TYPE constants:
|
||||
*
|
||||
* Currently, there are two chat types:
|
||||
*
|
||||
* - DC_CHAT_TYPE_SINGLE (100) - a normal chat is a chat with a single contact,
|
||||
* - @ref DC_CHAT_TYPE_SINGLE - a normal chat is a chat with a single contact,
|
||||
* chats_contacts contains one record for the user. DC_CONTACT_ID_SELF
|
||||
* (see dc_contact_t::id) is added _only_ for a self talk.
|
||||
*
|
||||
* - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts contain all group
|
||||
* - @ref DC_CHAT_TYPE_GROUP - a group chat, chats_contacts contain all group
|
||||
* members, incl. DC_CONTACT_ID_SELF
|
||||
*
|
||||
* - DC_CHAT_TYPE_MAILINGLIST (140) - a mailing list, this is similar to groups,
|
||||
* - @ref DC_CHAT_TYPE_MAILINGLIST - a mailing list, this is similar to groups,
|
||||
* however, the member list cannot be retrieved completely
|
||||
* and cannot be changed using this api.
|
||||
* moreover, for now, mailist lists are read-only.
|
||||
@@ -3216,18 +3246,6 @@ int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
|
||||
#define DC_MSG_ID_LAST_SPECIAL 9
|
||||
|
||||
|
||||
#define DC_STATE_UNDEFINED 0
|
||||
#define DC_STATE_IN_FRESH 10
|
||||
#define DC_STATE_IN_NOTICED 13
|
||||
#define DC_STATE_IN_SEEN 16
|
||||
#define DC_STATE_OUT_PREPARING 18
|
||||
#define DC_STATE_OUT_DRAFT 19
|
||||
#define DC_STATE_OUT_PENDING 20
|
||||
#define DC_STATE_OUT_FAILED 24
|
||||
#define DC_STATE_OUT_DELIVERED 26 // to check if a mail was sent, use dc_msg_is_sent()
|
||||
#define DC_STATE_OUT_MDN_RCVD 28
|
||||
|
||||
|
||||
/**
|
||||
* Create new message object. Message objects are needed e.g. for sending messages using
|
||||
* dc_send_msg(). Moreover, they are returned e.g. from dc_get_msg(),
|
||||
@@ -3307,28 +3325,37 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
|
||||
* Get the state of a message.
|
||||
*
|
||||
* Incoming message states:
|
||||
* - DC_STATE_IN_FRESH (10) - Incoming _fresh_ message. Fresh messages are neither noticed nor seen and are typically shown in notifications. Use dc_get_fresh_msgs() to get all fresh messages.
|
||||
* - DC_STATE_IN_NOTICED (13) - Incoming _noticed_ message. E.g. chat opened but message not yet read - noticed messages are not counted as unread but were not marked as read nor resulted in MDNs. Use dc_marknoticed_chat() to mark messages as being noticed.
|
||||
* - DC_STATE_IN_SEEN (16) - Incoming message, really _seen_ by the user. Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
|
||||
* - @ref DC_STATE_IN_FRESH - Incoming _fresh_ message.
|
||||
* Fresh messages are neither noticed nor seen and are typically shown in notifications.
|
||||
* Use dc_get_fresh_msgs() to get all fresh messages.
|
||||
* - @ref DC_STATE_IN_NOTICED - Incoming _noticed_ message.
|
||||
* E.g. chat opened but message not yet read.
|
||||
* Noticed messages are not counted as unread but were not marked as read nor resulted in MDNs.
|
||||
* Use dc_marknoticed_chat() to mark messages as being noticed.
|
||||
* - @ref DC_STATE_IN_SEEN - Incoming message, really _seen_ by the user.
|
||||
* Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
|
||||
*
|
||||
* Outgoing message states:
|
||||
* - DC_STATE_OUT_PREPARING (18) - For files which need time to be prepared before they can be sent,
|
||||
* the message enters this state before DC_STATE_OUT_PENDING.
|
||||
* - DC_STATE_OUT_DRAFT (19) - Message saved as draft using dc_set_draft()
|
||||
* - DC_STATE_OUT_PENDING (20) - The user has pressed the "send" button but the
|
||||
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
|
||||
* the message enters this state before @ref DC_STATE_OUT_PENDING.
|
||||
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
|
||||
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
|
||||
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
|
||||
* - DC_STATE_OUT_FAILED (24) - _Unrecoverable_ error (_recoverable_ errors result in pending messages), you'll receive the event #DC_EVENT_MSG_FAILED.
|
||||
* - DC_STATE_OUT_DELIVERED (26) - Outgoing message successfully delivered to server (one checkmark). Note, that already delivered messages may get into the state DC_STATE_OUT_FAILED if we get such a hint from the server.
|
||||
* - @ref DC_STATE_OUT_FAILED - _Unrecoverable_ error (_recoverable_ errors result in pending messages),
|
||||
* you'll receive the event #DC_EVENT_MSG_FAILED.
|
||||
* - @ref DC_STATE_OUT_DELIVERED - Outgoing message successfully delivered to server (one checkmark).
|
||||
* Note, that already delivered messages may get into the state @ref DC_STATE_OUT_FAILED if we get such a hint from the server.
|
||||
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_DELIVERED.
|
||||
* - DC_STATE_OUT_MDN_RCVD (28) - Outgoing message read by the recipient (two checkmarks; this requires goodwill on the receiver's side)
|
||||
* - @ref DC_STATE_OUT_MDN_RCVD - Outgoing message read by the recipient
|
||||
* (two checkmarks; this requires goodwill on the receiver's side)
|
||||
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_READ.
|
||||
* Also messages already read by some recipients
|
||||
* may get into the state DC_STATE_OUT_FAILED at a later point,
|
||||
* may get into the state @ref DC_STATE_OUT_FAILED at a later point,
|
||||
* e.g. when in a group, delivery fails for some recipients.
|
||||
*
|
||||
* If you just want to check if a message is sent or not, please use dc_msg_is_sent() which regards all states accordingly.
|
||||
*
|
||||
* The state of just created message objects is DC_STATE_UNDEFINED (0).
|
||||
* The state of just created message objects is @ref DC_STATE_UNDEFINED.
|
||||
* The state is always set by the core-library, users of the library cannot set the state directly, but it is changed implicitly e.g.
|
||||
* when calling dc_marknoticed_chat() or dc_markseen_msgs().
|
||||
*
|
||||
@@ -3592,7 +3619,7 @@ int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
|
||||
* Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable.
|
||||
* - dc_lot_t::text2: contains an excerpt of the message text.
|
||||
* - dc_lot_t::timestamp: the timestamp of the message.
|
||||
* - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
|
||||
* - dc_lot_t::state: The state of the message as one of the @ref DC_STATE constants.
|
||||
*
|
||||
* Typically used to display a search result. See also dc_chatlist_get_summary() to display a list of chats.
|
||||
*
|
||||
@@ -3906,6 +3933,31 @@ int dc_msg_get_videochat_type (const dc_msg_t* msg);
|
||||
int dc_msg_has_html (dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the message is completely downloaded
|
||||
* or if some further action is needed.
|
||||
*
|
||||
* Messages may be not fully downloaded
|
||||
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
|
||||
*
|
||||
* The function returns one of:
|
||||
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
||||
* and should be rendered as usual.
|
||||
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
||||
* In addition to the usual message rendering,
|
||||
* the UI shall show a download button that calls dc_download_full_msg()
|
||||
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
||||
* If the download fails or succeeds,
|
||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return One of the @ref DC_DOWNLOAD values
|
||||
*/
|
||||
int dc_msg_get_download_state (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Set the text of a message object.
|
||||
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
||||
@@ -4588,6 +4640,110 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_STATE DC_STATE
|
||||
*
|
||||
* These constants describe the state of a message.
|
||||
* The state can be retrieved using dc_msg_get_state()
|
||||
* and may change by various actions reported by various events
|
||||
*
|
||||
* @addtogroup DC_STATE
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Message just created. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_UNDEFINED 0
|
||||
|
||||
/**
|
||||
* Incoming fresh message. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_IN_FRESH 10
|
||||
|
||||
/**
|
||||
* Incoming noticed message. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_IN_NOTICED 13
|
||||
|
||||
/**
|
||||
* Incoming seen message. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_IN_SEEN 16
|
||||
|
||||
/**
|
||||
* Outgoing message being prepared. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_OUT_PREPARING 18
|
||||
|
||||
/**
|
||||
* Outgoing message drafted. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_OUT_DRAFT 19
|
||||
|
||||
/**
|
||||
* Outgoing message waiting to be sent. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_OUT_PENDING 20
|
||||
|
||||
/**
|
||||
* Outgoing message failed sending. See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_OUT_FAILED 24
|
||||
|
||||
/**
|
||||
* Outgoing message sent. To check if a mail was actually sent, use dc_msg_is_sent().
|
||||
* See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_OUT_DELIVERED 26
|
||||
|
||||
/**
|
||||
* Outgoing message sent and seen by recipients(s). See dc_msg_get_state() for details.
|
||||
*/
|
||||
#define DC_STATE_OUT_MDN_RCVD 28
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_CHAT_TYPE DC_CHAT_TYPE
|
||||
*
|
||||
* These constants describe the type of a chat.
|
||||
* The chat type can be retrieved using dc_chat_get_type()
|
||||
* and the type does not change during the chat's lifetime.
|
||||
*
|
||||
* @addtogroup DC_CHAT_TYPE
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Undefined chat type.
|
||||
* Normally, this type is not returned.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_UNDEFINED 0
|
||||
|
||||
/**
|
||||
* A one-to-one chat with a single contact. See dc_chat_get_type() for details.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_SINGLE 100
|
||||
|
||||
/**
|
||||
* A group chat. See dc_chat_get_type() for details.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_GROUP 120
|
||||
|
||||
/**
|
||||
* A mailing list. See dc_chat_get_type() for details.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_SOCKET DC_SOCKET
|
||||
*
|
||||
@@ -5014,8 +5170,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
* DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
||||
* A single message is sent successfully. State changed from @ref DC_STATE_OUT_PENDING to
|
||||
* @ref DC_STATE_OUT_DELIVERED.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
@@ -5025,8 +5181,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* A single message could not be sent.
|
||||
* State changed from DC_STATE_OUT_PENDING, DC_STATE_OUT_DELIVERED or DC_STATE_OUT_MDN_RCVD
|
||||
* to DC_STATE_OUT_FAILED, see dc_msg_get_state().
|
||||
* State changed from @ref DC_STATE_OUT_PENDING, @ref DC_STATE_OUT_DELIVERED or @ref DC_STATE_OUT_MDN_RCVD
|
||||
* to @ref DC_STATE_OUT_FAILED.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
@@ -5035,8 +5191,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
* DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
||||
* A single message is read by the receiver. State changed from @ref DC_STATE_OUT_DELIVERED to
|
||||
* @ref DC_STATE_OUT_MDN_RCVD.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
@@ -5280,6 +5436,44 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_CHAT_VISIBILITY_PINNED 2
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_DOWNLOAD DC_DOWNLOAD
|
||||
*
|
||||
* These constants describe the download state of a message.
|
||||
* The download state can be retrieved using dc_msg_get_download_state()
|
||||
* and usually changes after calling dc_download_full_msg().
|
||||
*
|
||||
* @addtogroup DC_DOWNLOAD
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Download not needed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_DONE 0
|
||||
|
||||
/**
|
||||
* Download available, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_AVAILABLE 10
|
||||
|
||||
/**
|
||||
* Download failed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_FAILURE 20
|
||||
|
||||
/**
|
||||
* Download in progress, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -5685,6 +5879,22 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by the percentage used
|
||||
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
|
||||
|
||||
/// "%1$s message"
|
||||
///
|
||||
/// Used as the message body when a message
|
||||
/// was not yet downloaded completely
|
||||
/// (dc_msg_get_download_state() is eg. @ref DC_DOWNLOAD_AVAILABLE).
|
||||
///
|
||||
/// `%1$s` will be replaced by human-readable size (eg. "1.2 MiB").
|
||||
#define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99
|
||||
|
||||
/// "Download maximum available until %1$s"
|
||||
///
|
||||
/// Appended after some separator to @ref DC_STR_PARTIAL_DOWNLOAD_MSG_BODY.
|
||||
///
|
||||
/// `%1$s` will be replaced by human-readable date and time.
|
||||
#define DC_STR_DOWNLOAD_AVAILABILITY 100
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -17,11 +17,13 @@ extern crate serde_json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_std::sync::RwLock;
|
||||
use async_std::task::{block_on, spawn};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
@@ -1570,6 +1572,8 @@ pub unsafe extern "C" fn dc_delete_msgs(
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
block_on(message::delete_msgs(ctx, &msg_ids))
|
||||
.log_err(ctx, "failed dc_delete_msgs() call")
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1648,6 +1652,18 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_download_full_msg(context: *mut dc_context_t, msg_id: u32) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_download_full_msg()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(MsgId::new(msg_id).download_full(ctx))
|
||||
.log_err(ctx, "Failed to download message fully.")
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_may_be_valid_addr(addr: *const libc::c_char) -> libc::c_int {
|
||||
if addr.is_null() {
|
||||
@@ -2047,7 +2063,9 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
seconds as i64,
|
||||
));
|
||||
))
|
||||
.log_err(ctx, "Failed dc_send_locations_to_chat()")
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2066,7 +2084,8 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id)) as libc::c_int
|
||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
|
||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2397,13 +2416,13 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
|
||||
let ctx = &*ffi_list.context;
|
||||
|
||||
block_on(async move {
|
||||
let lot = ffi_list
|
||||
let summary = ffi_list
|
||||
.list
|
||||
.get_summary(ctx, index as usize, maybe_chat)
|
||||
.await
|
||||
.log_err(ctx, "get_summary failed")
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(lot))
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2423,13 +2442,15 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
|
||||
} else {
|
||||
Some(MsgId::new(msg_id))
|
||||
};
|
||||
block_on(async move {
|
||||
let lot = Chatlist::get_summary2(ctx, ChatId::new(chat_id), msg_id, None)
|
||||
.await
|
||||
.log_err(ctx, "get_summary2 failed")
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(lot))
|
||||
})
|
||||
let summary = block_on(Chatlist::get_summary2(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
msg_id,
|
||||
None,
|
||||
))
|
||||
.log_err(ctx, "get_summary2 failed")
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2782,6 +2803,16 @@ pub unsafe extern "C" fn dc_msg_get_state(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
ffi_msg.message.get_state() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_download_state(msg: *mut dc_msg_t) -> libc::c_int {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_download_state()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.download_state() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_timestamp(msg: *mut dc_msg_t) -> i64 {
|
||||
if msg.is_null() {
|
||||
@@ -2971,10 +3002,10 @@ pub unsafe extern "C" fn dc_msg_get_summary(
|
||||
let ffi_msg = &mut *msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(async move {
|
||||
let lot = ffi_msg.message.get_summary(ctx, maybe_chat).await;
|
||||
Box::into_raw(Box::new(lot))
|
||||
})
|
||||
let summary = block_on(async move { ffi_msg.message.get_summary(ctx, maybe_chat).await })
|
||||
.log_err(ctx, "dc_msg_get_summary failed")
|
||||
.unwrap_or_default();
|
||||
Box::into_raw(Box::new(summary.into()))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3683,8 +3714,29 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
||||
|
||||
// -- Accounts
|
||||
|
||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||
/// `dc_accounts_t` in multiple threads at once.
|
||||
pub struct AccountsWrapper {
|
||||
inner: RwLock<Accounts>,
|
||||
}
|
||||
|
||||
impl Deref for AccountsWrapper {
|
||||
type Target = RwLock<Accounts>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountsWrapper {
|
||||
fn new(accounts: Accounts) -> Self {
|
||||
let inner = RwLock::new(accounts);
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a list of deltachat accounts.
|
||||
pub type dc_accounts_t = Accounts;
|
||||
pub type dc_accounts_t = AccountsWrapper;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
@@ -3707,7 +3759,7 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
let accs = block_on(Accounts::new(os_name, as_path(dbfile).to_path_buf().into()));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(accs)),
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
Err(err) => {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
eprintln!("failed to create accounts: {:#}", err);
|
||||
@@ -3739,7 +3791,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.get_account(id))
|
||||
block_on(async move { accounts.read().await.get_account(id).await })
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -3754,7 +3806,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.get_selected_account())
|
||||
block_on(async move { accounts.read().await.get_selected_account().await })
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -3770,7 +3822,7 @@ pub unsafe extern "C" fn dc_accounts_select_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.select_account(id))
|
||||
block_on(async move { accounts.write().await.select_account(id).await })
|
||||
.map(|_| 1)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -3782,9 +3834,9 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let accounts = &mut *accounts;
|
||||
|
||||
block_on(accounts.add_account()).unwrap_or(0)
|
||||
block_on(async move { accounts.write().await.add_account().await }).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3797,9 +3849,9 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let accounts = &mut *accounts;
|
||||
|
||||
block_on(accounts.remove_account(id))
|
||||
block_on(async move { accounts.write().await.remove_account(id).await })
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
@@ -3814,12 +3866,18 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let accounts = &mut *accounts;
|
||||
let dbfile = to_string_lossy(dbfile);
|
||||
|
||||
block_on(accounts.migrate_account(async_std::path::PathBuf::from(dbfile)))
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
block_on(async move {
|
||||
accounts
|
||||
.write()
|
||||
.await
|
||||
.migrate_account(async_std::path::PathBuf::from(dbfile))
|
||||
.await
|
||||
})
|
||||
.map(|_| 1)
|
||||
.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3830,7 +3888,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let list = block_on(accounts.get_all());
|
||||
let list = block_on(async move { accounts.read().await.get_all().await });
|
||||
let array: dc_array_t = list.into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
@@ -3843,7 +3901,7 @@ pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t)
|
||||
return 0;
|
||||
}
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.all_work_done().await as libc::c_int })
|
||||
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3854,7 +3912,7 @@ pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.start_io());
|
||||
block_on(async move { accounts.read().await.start_io().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3865,7 +3923,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.stop_io());
|
||||
block_on(async move { accounts.read().await.stop_io().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3876,7 +3934,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.maybe_network());
|
||||
block_on(async move { accounts.read().await.maybe_network().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3887,7 +3945,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.maybe_network_lost());
|
||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||
}
|
||||
|
||||
pub type dc_accounts_event_emitter_t = deltachat::accounts::EventEmitter;
|
||||
@@ -3902,7 +3960,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let emitter = block_on(accounts.get_event_emitter());
|
||||
let emitter = block_on(async move { accounts.read().await.get_event_emitter().await });
|
||||
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ license = "MPL-2.0"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.74"
|
||||
syn = "1.0.76"
|
||||
quote = "1.0.2"
|
||||
|
||||
@@ -13,10 +13,10 @@ use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::lot::LotState;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
@@ -189,10 +189,18 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
MessageState::OutFailed => " !!",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let downloadstate = match msg.download_state() {
|
||||
DownloadState::Done => "",
|
||||
DownloadState::Available => " [⬇ Download available]",
|
||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
};
|
||||
|
||||
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
|
||||
prefix.as_ref(),
|
||||
msg.get_id(),
|
||||
if msg.get_showpadlock() { "🔒" } else { "" },
|
||||
@@ -226,6 +234,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
""
|
||||
},
|
||||
statestr,
|
||||
downloadstate,
|
||||
&temp2,
|
||||
);
|
||||
}
|
||||
@@ -289,7 +298,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
|
||||
"addr unset"
|
||||
}
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
let peerstate = Peerstate::from_addr(context, addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
@@ -394,6 +403,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
download <msg-id>\n\
|
||||
html <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
@@ -450,7 +460,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <setup-code> expected"
|
||||
);
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
|
||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
||||
}
|
||||
"has-backup" => {
|
||||
has_backup(&context, blobdir).await?;
|
||||
@@ -497,13 +507,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"set" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let key = config::Config::from_str(arg1)?;
|
||||
let value = if arg2.is_empty() { None } else { Some(arg2) };
|
||||
context.set_config(key, value).await?;
|
||||
}
|
||||
"get" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <key> missing.");
|
||||
let key = config::Config::from_str(&arg1)?;
|
||||
let key = config::Config::from_str(arg1)?;
|
||||
let val = context.get_config(key).await;
|
||||
println!("{}={:?}", key, val);
|
||||
}
|
||||
@@ -569,26 +579,25 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
""
|
||||
},
|
||||
);
|
||||
let lot = chatlist.get_summary(&context, i, Some(&chat)).await?;
|
||||
let summary = chatlist.get_summary(&context, i, Some(&chat)).await?;
|
||||
let statestr = if chat.visibility == ChatVisibility::Archived {
|
||||
" [Archived]"
|
||||
} else {
|
||||
match lot.get_state() {
|
||||
LotState::MsgOutPending => " o",
|
||||
LotState::MsgOutDelivered => " √",
|
||||
LotState::MsgOutMdnRcvd => " √√",
|
||||
LotState::MsgOutFailed => " !!",
|
||||
match summary.state {
|
||||
MessageState::OutPending => " o",
|
||||
MessageState::OutDelivered => " √",
|
||||
MessageState::OutMdnRcvd => " √√",
|
||||
MessageState::OutFailed => " !!",
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
let timestr = dc_timestamp_to_str(lot.get_timestamp());
|
||||
let text1 = lot.get_text1();
|
||||
let text2 = lot.get_text2();
|
||||
let timestr = dc_timestamp_to_str(summary.timestamp);
|
||||
println!(
|
||||
"{}{}{}{} [{}]{}",
|
||||
text1.unwrap_or(""),
|
||||
if text1.is_some() { ": " } else { "" },
|
||||
text2.unwrap_or(""),
|
||||
"{}{}{} [{}]{}",
|
||||
summary
|
||||
.prefix
|
||||
.map_or_else(String::new, |prefix| format!("{}: ", prefix)),
|
||||
summary.text,
|
||||
statestr,
|
||||
×tr,
|
||||
if chat.is_sending_locations() {
|
||||
@@ -602,7 +611,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if location::is_sending_locations_to_chat(&context, None).await {
|
||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{} chats", cnt);
|
||||
@@ -773,7 +782,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
)
|
||||
.await,
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
"getlocations" => {
|
||||
@@ -818,7 +827,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
seconds,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
println!(
|
||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -895,12 +904,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
let chat = if let Some(ref sel_chat) = sel_chat {
|
||||
Some(sel_chat.get_id())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist = context.search_msgs(chat, arg1).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
@@ -1030,6 +1034,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
println!("{}", res);
|
||||
}
|
||||
"download" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
println!("Scheduling download for {:?}", id);
|
||||
id.download_full(&context).await?;
|
||||
}
|
||||
"html" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
@@ -1067,7 +1077,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut ids = [MsgId::new(0); 1];
|
||||
ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::delete_msgs(&context, &ids).await;
|
||||
message::delete_msgs(&context, &ids).await?;
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
|
||||
@@ -202,13 +202,14 @@ const CHAT_COMMANDS: [&str; 33] = [
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 6] = [
|
||||
const MESSAGE_COMMANDS: [&str; 7] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
@@ -325,7 +326,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
|
||||
loop {
|
||||
let p = "> ";
|
||||
let readline = rl.readline(&p);
|
||||
let readline = rl.readline(p);
|
||||
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
@@ -408,7 +409,7 @@ async fn handle_cmd(
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
if let Some(mut qr) = dc_get_securejoin_qr(&ctx, group).await {
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
|
||||
183
src/accounts.rs
183
src/accounts.rs
@@ -16,11 +16,11 @@ use crate::context::Context;
|
||||
use crate::events::Event;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Accounts {
|
||||
dir: PathBuf,
|
||||
config: Config,
|
||||
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
|
||||
accounts: BTreeMap<u32, Context>,
|
||||
emitter: EventEmitter,
|
||||
|
||||
/// Sender side of the fake event channel.
|
||||
@@ -76,7 +76,7 @@ impl Accounts {
|
||||
Ok(Self {
|
||||
dir,
|
||||
config,
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
accounts,
|
||||
emitter,
|
||||
fake_sender,
|
||||
})
|
||||
@@ -84,13 +84,13 @@ impl Accounts {
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.read().await.get(&id).cloned()
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub async fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account().await;
|
||||
self.accounts.read().await.get(&id).cloned()
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the currently selected account's id or None if no account is selected.
|
||||
@@ -102,27 +102,27 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Select the given account.
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
self.config.select_account(id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account.
|
||||
pub async fn add_account(&self) -> Result<u32> {
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
let os_name = self.config.os_name().await;
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.write().await.remove(&id);
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.remove(&id);
|
||||
ensure!(ctx.is_some(), "no account with this id: {}", id);
|
||||
let ctx = ctx.unwrap();
|
||||
ctx.stop_io().await;
|
||||
@@ -139,7 +139,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Migrate an existing account into this structure.
|
||||
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
|
||||
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
|
||||
let blobdir = Context::derive_blobdir(&dbfile);
|
||||
let walfile = Context::derive_walfile(&dbfile);
|
||||
|
||||
@@ -195,7 +195,7 @@ impl Accounts {
|
||||
)
|
||||
.await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.write().await.insert(account_config.id, ctx);
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -216,7 +216,7 @@ impl Accounts {
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.read().await.keys().copied().collect()
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
||||
@@ -230,7 +230,7 @@ impl Accounts {
|
||||
/// - while dc_accounts_all_work_done() returns false:
|
||||
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
for account in self.accounts.read().await.values() {
|
||||
for account in self.accounts.values() {
|
||||
if !account.all_work_done().await {
|
||||
return false;
|
||||
}
|
||||
@@ -239,29 +239,25 @@ impl Accounts {
|
||||
}
|
||||
|
||||
pub async fn start_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_io(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.stop_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_network(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn maybe_network_lost(&self) {
|
||||
let accounts = &*self.accounts.read().await;
|
||||
for account in accounts.values() {
|
||||
for account in self.accounts.values() {
|
||||
account.maybe_network_lost().await;
|
||||
}
|
||||
}
|
||||
@@ -343,12 +339,15 @@ pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
pub const DB_NAME: &str = "dc.db";
|
||||
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
file: PathBuf,
|
||||
inner: Arc<RwLock<InnerConfig>>,
|
||||
inner: InnerConfig,
|
||||
}
|
||||
|
||||
/// Account manager configuration file contents.
|
||||
///
|
||||
/// This is serialized into TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
struct InnerConfig {
|
||||
pub os_name: String,
|
||||
@@ -360,14 +359,15 @@ struct InnerConfig {
|
||||
|
||||
impl Config {
|
||||
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
|
||||
let inner = InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
let cfg = Config {
|
||||
file: dir.join(CONFIG_NAME),
|
||||
inner: Arc::new(RwLock::new(InnerConfig {
|
||||
os_name,
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
})),
|
||||
inner,
|
||||
};
|
||||
|
||||
cfg.sync().await?;
|
||||
@@ -376,17 +376,14 @@ impl Config {
|
||||
}
|
||||
|
||||
pub async fn os_name(&self) -> String {
|
||||
self.inner.read().await.os_name.clone()
|
||||
self.inner.os_name.clone()
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(
|
||||
&self.file,
|
||||
toml::to_string_pretty(&*self.inner.read().await)?,
|
||||
)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
@@ -394,18 +391,14 @@ impl Config {
|
||||
let bytes = fs::read(&file).await.context("failed to read file")?;
|
||||
let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?;
|
||||
|
||||
Ok(Config {
|
||||
file,
|
||||
inner: Arc::new(RwLock::new(inner)),
|
||||
})
|
||||
Ok(Config { file, inner })
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let cfg = &*self.inner.read().await;
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &cfg.accounts {
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(
|
||||
cfg.os_name.clone(),
|
||||
self.inner.os_name.clone(),
|
||||
account_config.dbfile().into(),
|
||||
account_config.id,
|
||||
)
|
||||
@@ -417,19 +410,18 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Create a new account in the given root directory.
|
||||
async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
async fn new_account(&mut self, dir: &PathBuf) -> Result<AccountConfig> {
|
||||
let id = {
|
||||
let inner = &mut self.inner.write().await;
|
||||
let id = inner.next_id;
|
||||
let id = self.inner.next_id;
|
||||
let uuid = Uuid::new_v4();
|
||||
let target_dir = dir.join(uuid.to_simple_ref().to_string());
|
||||
|
||||
inner.accounts.push(AccountConfig {
|
||||
self.inner.accounts.push(AccountConfig {
|
||||
id,
|
||||
dir: target_dir.into(),
|
||||
uuid,
|
||||
});
|
||||
inner.next_id += 1;
|
||||
self.inner.next_id += 1;
|
||||
id
|
||||
};
|
||||
|
||||
@@ -441,16 +433,16 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Removes an existing acccount entirely.
|
||||
pub async fn remove_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
if let Some(idx) = inner.accounts.iter().position(|e| e.id == id) {
|
||||
if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
|
||||
// remove account from the configs
|
||||
inner.accounts.remove(idx);
|
||||
self.inner.accounts.remove(idx);
|
||||
}
|
||||
if inner.selected_account == id {
|
||||
if self.inner.selected_account == id {
|
||||
// reset selected account
|
||||
inner.selected_account = inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
self.inner.selected_account =
|
||||
self.inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,29 +450,22 @@ impl Config {
|
||||
}
|
||||
|
||||
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.cloned()
|
||||
self.inner.accounts.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
self.inner.read().await.selected_account
|
||||
self.inner.selected_account
|
||||
}
|
||||
|
||||
pub async fn select_account(&self, id: u32) -> Result<()> {
|
||||
pub async fn select_account(&mut self, id: u32) -> Result<()> {
|
||||
{
|
||||
let inner = &mut *self.inner.write().await;
|
||||
ensure!(
|
||||
inner.accounts.iter().any(|e| e.id == id),
|
||||
self.inner.accounts.iter().any(|e| e.id == id),
|
||||
"invalid account id: {}",
|
||||
id
|
||||
);
|
||||
|
||||
inner.selected_account = id;
|
||||
self.inner.selected_account = id;
|
||||
}
|
||||
|
||||
self.sync().await?;
|
||||
@@ -514,23 +499,17 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1").into();
|
||||
|
||||
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(
|
||||
&*accounts1.config.inner.read().await,
|
||||
&*accounts2.config.inner.read().await,
|
||||
);
|
||||
assert_eq!(
|
||||
accounts1.accounts.read().await.len(),
|
||||
accounts2.accounts.read().await.len()
|
||||
);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -538,26 +517,26 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 0);
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 2);
|
||||
assert_eq!(accounts.accounts.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -565,14 +544,14 @@ mod tests {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let id = accounts.add_account().await?;
|
||||
assert!(accounts.get_selected_account().await.is_some());
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
|
||||
accounts.remove_account(id).await?;
|
||||
@@ -586,8 +565,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 0);
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other").into();
|
||||
@@ -604,7 +583,7 @@ mod tests {
|
||||
.migrate_account(extern_dbfile.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.read().await.len(), 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
@@ -623,7 +602,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
@@ -643,7 +622,7 @@ mod tests {
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
@@ -726,7 +705,7 @@ mod tests {
|
||||
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.read().await.len(), 0);
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
// Create event emitter.
|
||||
let mut event_emitter = accounts.get_event_emitter().await;
|
||||
@@ -743,4 +722,28 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_account_new_add_remove_with_active_event_emmitter() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
accounts.add_account().await.unwrap();
|
||||
accounts.add_account().await.unwrap();
|
||||
|
||||
// Create event emitter.
|
||||
let mut event_emitter = accounts.get_event_emitter().await;
|
||||
|
||||
// make sure that account is still removed on windows
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
|
||||
// make sure event emitter is not dropped before the test
|
||||
println!("{:?}", event_emitter.recv().await);
|
||||
}
|
||||
}
|
||||
|
||||
76
src/chat.rs
76
src/chat.rs
@@ -183,7 +183,7 @@ impl ChatId {
|
||||
|| contact_id == DC_CONTACT_ID_SELF
|
||||
{
|
||||
let chat_id = ChatId::get_for_contact(context, contact_id).await?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
||||
chat_id
|
||||
} else {
|
||||
warn!(
|
||||
@@ -284,7 +284,7 @@ impl ChatId {
|
||||
for contact_id in get_chat_contacts(context, self).await? {
|
||||
if contact_id != DC_CONTACT_ID_SELF {
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat)
|
||||
.await;
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,7 +497,7 @@ impl ChatId {
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
|
||||
job::kill_action(context, Action::Housekeeping).await;
|
||||
job::kill_action(context, Action::Housekeeping).await?;
|
||||
let j = job::Job::new(Action::Housekeeping, 0, Params::new(), 10);
|
||||
job::add(context, j).await;
|
||||
|
||||
@@ -1099,10 +1099,9 @@ impl Chat {
|
||||
if self.typ == Chattype::Group
|
||||
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await
|
||||
{
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ErrorSelfNotInGroup("Cannot send message; self not in group.".into())
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot send message; self not in group.".into(),
|
||||
));
|
||||
bail!("Cannot set message; self not in group.");
|
||||
}
|
||||
|
||||
@@ -1223,7 +1222,7 @@ impl Chat {
|
||||
};
|
||||
let ephemeral_timestamp = match ephemeral_timer {
|
||||
EphemeralTimer::Disabled => 0,
|
||||
EphemeralTimer::Enabled { duration } => time() + i64::from(duration),
|
||||
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
|
||||
};
|
||||
|
||||
let new_mime_headers = if msg.has_html() {
|
||||
@@ -2273,12 +2272,9 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into()
|
||||
)
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into(),
|
||||
));
|
||||
bail!("can not add contact because our account is not part of it");
|
||||
}
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
@@ -2450,19 +2446,15 @@ impl rusqlite::types::FromSql for MuteDuration {
|
||||
|
||||
pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<()> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
if context
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![duration, chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("Failed to set mute duration, chat might not exist -");
|
||||
}
|
||||
.context(format!("Failed to set mute duration for {}", chat_id))?;
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2489,12 +2481,9 @@ pub async fn remove_contact_from_chat(
|
||||
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if chat.typ == Chattype::Group {
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ErrorSelfNotInGroup(
|
||||
"Cannot remove contact from chat; self not in group.".into()
|
||||
)
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot remove contact from chat; self not in group.".into(),
|
||||
));
|
||||
} else {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if chat.is_promoted() {
|
||||
@@ -2586,10 +2575,9 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
if chat.name == new_name {
|
||||
success = true;
|
||||
} else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ErrorSelfNotInGroup("Cannot set chat name; self not in group".into())
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot set chat name; self not in group".into(),
|
||||
));
|
||||
} else {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if context
|
||||
@@ -2653,12 +2641,9 @@ pub async fn set_chat_profile_image(
|
||||
);
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ErrorSelfNotInGroup(
|
||||
"Cannot set chat profile image; self not in group.".into()
|
||||
)
|
||||
);
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot set chat profile image; self not in group.".into(),
|
||||
));
|
||||
bail!("Failed to set profile image");
|
||||
}
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -2686,15 +2671,12 @@ pub async fn set_chat_profile_image(
|
||||
chat.update_param(context).await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id
|
||||
}
|
||||
);
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
emit_event!(context, EventType::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3252,7 +3234,9 @@ mod tests {
|
||||
assert!(chat.get_profile_image(&t).await.unwrap().is_some());
|
||||
|
||||
// delete device message, make sure it is not added again
|
||||
message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()]).await;
|
||||
message::delete_msgs(&t, &[*msg1_id.as_ref().unwrap()])
|
||||
.await
|
||||
.unwrap();
|
||||
let msg1 = message::Message::load_from_db(&t, *msg1_id.as_ref().unwrap()).await;
|
||||
assert!(msg1.is_err() || msg1.unwrap().chat_id.is_trash());
|
||||
let msg3_id = add_device_msg(&t, Some("any-label"), Some(&mut msg2)).await;
|
||||
|
||||
@@ -11,9 +11,9 @@ use crate::constants::{
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::ephemeral::delete_expired_messages;
|
||||
use crate::lot::Lot;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::stock_str;
|
||||
use crate::summary::Summary;
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
///
|
||||
@@ -288,26 +288,13 @@ impl Chatlist {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a summary for a chatlist index.
|
||||
///
|
||||
/// The summary is returned by a dc_lot_t object with the following fields:
|
||||
///
|
||||
/// - dc_lot_t::text1: contains the username or the strings "Me", "Draft" and so on.
|
||||
/// The string may be colored by having a look at text1_meaning.
|
||||
/// If there is no such name or it should not be displayed, the element is NULL.
|
||||
/// - dc_lot_t::text1_meaning: one of DC_TEXT1_USERNAME, DC_TEXT1_SELF or DC_TEXT1_DRAFT.
|
||||
/// Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable.
|
||||
/// - dc_lot_t::text2: contains an excerpt of the message text or strings as
|
||||
/// "No messages". May be NULL of there is no such text (eg. for the archive link)
|
||||
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
|
||||
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
|
||||
// 0 if not applicable.
|
||||
/// Returns a summary for a given chatlist index.
|
||||
pub async fn get_summary(
|
||||
&self,
|
||||
context: &Context,
|
||||
index: usize,
|
||||
chat: Option<&Chat>,
|
||||
) -> Result<Lot> {
|
||||
) -> Result<Summary> {
|
||||
// The summary is created by the chat, not by the last message.
|
||||
// This is because we may want to display drafts here or stuff as
|
||||
// "is typing".
|
||||
@@ -320,14 +307,13 @@ impl Chatlist {
|
||||
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
|
||||
}
|
||||
|
||||
/// Returns a summary for a given chatlist item.
|
||||
pub async fn get_summary2(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
lastmsg_id: Option<MsgId>,
|
||||
chat: Option<&Chat>,
|
||||
) -> Result<Lot> {
|
||||
let mut ret = Lot::new();
|
||||
|
||||
) -> Result<Summary> {
|
||||
let chat_loaded: Chat;
|
||||
let chat = if let Some(chat) = chat {
|
||||
chat
|
||||
@@ -344,9 +330,8 @@ impl Chatlist {
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
let lastcontact =
|
||||
Contact::load_from_db(context, lastmsg.from_id).await.ok();
|
||||
(Some(lastmsg), lastcontact)
|
||||
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
}
|
||||
@@ -356,17 +341,15 @@ impl Chatlist {
|
||||
};
|
||||
|
||||
if chat.id.is_archived_link() {
|
||||
ret.text2 = None;
|
||||
} else if let Some(mut lastmsg) =
|
||||
lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED)
|
||||
{
|
||||
ret.fill(&mut lastmsg, chat, lastcontact.as_ref(), context)
|
||||
.await;
|
||||
Ok(Default::default())
|
||||
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) {
|
||||
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
|
||||
} else {
|
||||
ret.text2 = Some(stock_str::no_messages(context).await);
|
||||
Ok(Summary {
|
||||
text: stock_str::no_messages(context).await,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
|
||||
@@ -637,6 +620,6 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
let summary = chats.get_summary(&t, 0, None).await.unwrap();
|
||||
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
|
||||
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,11 @@ pub enum Config {
|
||||
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -317,7 +322,7 @@ impl Context {
|
||||
.set_raw_config(key, value)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
job::schedule_resync(self).await;
|
||||
job::schedule_resync(self).await?;
|
||||
ret
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -319,7 +319,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
.filter(|params| params.protocol == Protocol::Smtp)
|
||||
.cloned()
|
||||
.collect();
|
||||
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
|
||||
let provider_strict_tls = param
|
||||
.provider
|
||||
.map_or(socks5_config.is_some(), |provider| provider.strict_tls);
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp_configured = false;
|
||||
|
||||
@@ -1109,15 +1109,19 @@ impl Contact {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn scaleup_origin_by_id(context: &Context, contact_id: u32, origin: Origin) -> bool {
|
||||
pub async fn scaleup_origin_by_id(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id as i32, origin],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -371,6 +371,12 @@ impl Context {
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"download_limit",
|
||||
self.get_config_int(Config::DownloadLimit)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert("inbox_watch", inbox_watch.to_string());
|
||||
res.insert("sentbox_watch", sentbox_watch.to_string());
|
||||
res.insert("mvbox_watch", mvbox_watch.to_string());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Result};
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use itertools::join;
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -19,8 +19,9 @@ use crate::constants::{
|
||||
use crate::contact::{addr_cmp, normalize_name, Contact, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{
|
||||
dc_create_smeared_timestamp, dc_extract_grpid_from_rfc724_mid, dc_smeared_time, time,
|
||||
dc_create_smeared_timestamp, dc_extract_grpid_from_rfc724_mid, dc_smeared_time,
|
||||
};
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -57,15 +58,27 @@ pub async fn dc_receive_imf(
|
||||
server_uid: u32,
|
||||
seen: bool,
|
||||
) -> Result<()> {
|
||||
dc_receive_imf_inner(context, imf_raw, server_folder, server_uid, seen, false).await
|
||||
dc_receive_imf_inner(
|
||||
context,
|
||||
imf_raw,
|
||||
server_folder,
|
||||
server_uid,
|
||||
seen,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// If `is_partial_download` is set, it contains the full message size in bytes.
|
||||
/// Do not confuse that with `replace_partial_download` that will be set when the full message is loaded later.
|
||||
pub(crate) async fn dc_receive_imf_inner(
|
||||
context: &Context,
|
||||
imf_raw: &[u8],
|
||||
server_folder: &str,
|
||||
server_uid: u32,
|
||||
seen: bool,
|
||||
is_partial_download: Option<u32>,
|
||||
fetching_existing_messages: bool,
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
@@ -78,13 +91,14 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
println!("{}", String::from_utf8_lossy(imf_raw));
|
||||
}
|
||||
|
||||
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw).await {
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf: can't parse MIME: {}", err);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(mime_parser) => mime_parser,
|
||||
};
|
||||
let mut mime_parser =
|
||||
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
|
||||
Err(err) => {
|
||||
warn!(context, "dc_receive_imf: can't parse MIME: {}", err);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(mime_parser) => mime_parser,
|
||||
};
|
||||
|
||||
// we can not add even an empty record if we have no info whatsoever
|
||||
if !mime_parser.has_headers() {
|
||||
@@ -111,19 +125,31 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
"received message {} has Message-Id: {}", server_uid, rfc724_mid
|
||||
);
|
||||
|
||||
// check, if the mail is already in our database - if so, just update the folder/uid
|
||||
// (if the mail was moved around) and finish. (we may get a mail twice eg. if it is
|
||||
// moved between folders. make sure, this check is done eg. before securejoin-processing) */
|
||||
if let Some((old_server_folder, old_server_uid, _)) =
|
||||
// check, if the mail is already in our database.
|
||||
// make sure, this check is done eg. before securejoin-processing.
|
||||
let replace_partial_download = if let Some((old_server_folder, old_server_uid, old_msg_id)) =
|
||||
message::rfc724_mid_exists(context, &rfc724_mid).await?
|
||||
{
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
message::update_server_uid(context, &rfc724_mid, server_folder, server_uid).await;
|
||||
let msg = Message::load_from_db(context, old_msg_id).await?;
|
||||
if msg.download_state() != DownloadState::Done && is_partial_download.is_none() {
|
||||
// the mesage was partially downloaded before and is fully downloaded now.
|
||||
info!(
|
||||
context,
|
||||
"Message already partly in DB, replacing by full message."
|
||||
);
|
||||
old_msg_id.delete_from_db(context).await?;
|
||||
true
|
||||
} else {
|
||||
// the message was probably moved around.
|
||||
info!(context, "Message already in DB, updating folder/uid.");
|
||||
if old_server_folder != server_folder || old_server_uid != server_uid {
|
||||
message::update_server_uid(context, &rfc724_mid, server_folder, server_uid).await;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
warn!(context, "Message already in DB");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// the function returns the number of created messages in the database
|
||||
let mut hidden = false;
|
||||
@@ -181,7 +207,8 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
&mut sent_timestamp,
|
||||
from_id,
|
||||
&mut hidden,
|
||||
seen,
|
||||
seen || replace_partial_download,
|
||||
is_partial_download,
|
||||
&mut needs_delete_job,
|
||||
&mut created_db_entries,
|
||||
&mut create_event_to_send,
|
||||
@@ -210,27 +237,39 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||
match contact::set_profile_image(
|
||||
context,
|
||||
from_id,
|
||||
avatar_action,
|
||||
mime_parser.was_encrypted(),
|
||||
)
|
||||
.await
|
||||
if from_id != 0
|
||||
&& context
|
||||
.update_contacts_timestamp(from_id, Param::AvatarTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
Ok(()) => {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf cannot update profile image: {}", err);
|
||||
}
|
||||
};
|
||||
match contact::set_profile_image(
|
||||
context,
|
||||
from_id,
|
||||
avatar_action,
|
||||
mime_parser.was_encrypted(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf cannot update profile image: {}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Always update the status, even if there is no footer, to allow removing the status.
|
||||
//
|
||||
// Ignore MDNs though, as they never contain the signature even if user has set it.
|
||||
if mime_parser.mdn_reports.is_empty() {
|
||||
if mime_parser.mdn_reports.is_empty()
|
||||
&& is_partial_download.is_none()
|
||||
&& from_id != 0
|
||||
&& context
|
||||
.update_contacts_timestamp(from_id, Param::StatusTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
if let Err(err) = contact::set_status(
|
||||
context,
|
||||
from_id,
|
||||
@@ -248,7 +287,7 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
let delete_server_after = context.get_config_delete_server_after().await?;
|
||||
|
||||
if !created_db_entries.is_empty() {
|
||||
if needs_delete_job || delete_server_after == Some(0) {
|
||||
if needs_delete_job || (delete_server_after == Some(0) && is_partial_download.is_none()) {
|
||||
for db_entry in &created_db_entries {
|
||||
job::add(
|
||||
context,
|
||||
@@ -366,6 +405,7 @@ async fn add_parts(
|
||||
from_id: u32,
|
||||
hidden: &mut bool,
|
||||
seen: bool,
|
||||
is_partial_download: Option<u32>,
|
||||
needs_delete_job: &mut bool,
|
||||
created_db_entries: &mut Vec<(ChatId, MsgId)>,
|
||||
create_event_to_send: &mut Option<CreateEvent>,
|
||||
@@ -410,6 +450,9 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
let rcvd_timestamp = dc_smeared_time(context).await;
|
||||
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
|
||||
|
||||
// check if the message introduces a new chat:
|
||||
// - outgoing messages introduce a chat with the first to: address if they are sent by a messenger
|
||||
// - incoming messages introduce a chat only for known contacts if they are sent by a messenger
|
||||
@@ -483,6 +526,7 @@ async fn add_parts(
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
*sent_timestamp,
|
||||
if test_normal_chat.is_none() {
|
||||
allow_creation
|
||||
} else {
|
||||
@@ -603,7 +647,7 @@ async fn add_parts(
|
||||
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
|
||||
// the contact requests will pop up and this should be just fine.
|
||||
Contact::scaleup_origin_by_id(context, from_id, Origin::IncomingReplyTo)
|
||||
.await;
|
||||
.await?;
|
||||
info!(
|
||||
context,
|
||||
"Message is a reply to a known message, mark sender as known.",
|
||||
@@ -712,6 +756,7 @@ async fn add_parts(
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
&mut mime_parser,
|
||||
*sent_timestamp,
|
||||
allow_creation,
|
||||
Blocked::Not,
|
||||
from_id,
|
||||
@@ -805,41 +850,79 @@ async fn add_parts(
|
||||
let location_kml_is = mime_parser.location_kml.is_some();
|
||||
|
||||
// correct message_timestamp, it should not be used before,
|
||||
// however, we cannot do this earlier as we need from_id to be set
|
||||
// however, we cannot do this earlier as we need chat_id to be set
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
let rcvd_timestamp = time();
|
||||
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, chat_id, in_fresh).await?;
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
//
|
||||
// Only non-hidden timers are applied now. Timers from hidden
|
||||
// Only non-hidden timers are applied. Timers from hidden
|
||||
// messages such as read receipts can be useful to detect
|
||||
// ephemeral timer support, but timer changes without visible
|
||||
// received messages may be confusing to the user.
|
||||
if !*hidden
|
||||
&& !location_kml_is
|
||||
&& !is_mdn
|
||||
&& (is_dc_message != MessengerMessage::Yes
|
||||
|| parent.is_none()
|
||||
|| parent.unwrap().ephemeral_timer != ephemeral_timer)
|
||||
&& chat_id.get_ephemeral_timer(context).await? != ephemeral_timer
|
||||
{
|
||||
if let Err(err) = chat_id
|
||||
.inner_set_ephemeral_timer(context, ephemeral_timer)
|
||||
.await
|
||||
info!(
|
||||
context,
|
||||
"received new ephemeral timer value {:?} for chat {}, checking if it should be applied",
|
||||
ephemeral_timer,
|
||||
chat_id
|
||||
);
|
||||
if is_dc_message == MessengerMessage::Yes
|
||||
&& mime_parser
|
||||
.parts
|
||||
.get(0)
|
||||
.and_then(|part| part.param.get(Param::Quote))
|
||||
.is_none()
|
||||
&& parent.map(|p| p.ephemeral_timer) == Some(ephemeral_timer)
|
||||
{
|
||||
// The message is a Delta Chat message without a quote, so it must be a reply to the
|
||||
// last message in the chat as seen by the sender. The timer is the same in both the
|
||||
// received message and its parent, so we know that the sender has not seen any change
|
||||
// of the timer between these messages. As our timer value is different, it means the
|
||||
// sender has not received some timer update that we have seen or sent ourselves, so we
|
||||
// ignore incoming timer to prevent a rollback.
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
"ignoring ephemeral timer change to {:?} for chat {} to avoid rollback",
|
||||
ephemeral_timer,
|
||||
chat_id
|
||||
);
|
||||
} else if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
|
||||
chat::add_info_msg(
|
||||
} else if chat_id
|
||||
.update_timestamp(context, Param::EphemeralSettingsTimestamp, *sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
if let Err(err) = chat_id
|
||||
.inner_set_ephemeral_timer(context, ephemeral_timer)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"failed to modify timer for chat {}: {}", chat_id, err
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"updated ephemeral timer to {:?} for chat {}", ephemeral_timer, chat_id
|
||||
);
|
||||
if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
sort_timestamp,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
chat_id,
|
||||
stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
|
||||
sort_timestamp,
|
||||
)
|
||||
.await;
|
||||
"ignoring ephemeral timer change to {:?} because it's outdated", ephemeral_timer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,8 +941,8 @@ async fn add_parts(
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
}
|
||||
|
||||
// if a chat is protected, check additional properties
|
||||
if !chat_id.is_special() {
|
||||
// if a chat is protected and the message is fully downloaded, check additional properties
|
||||
if !chat_id.is_special() && is_partial_download.is_none() {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
let new_status = match mime_parser.is_system_message {
|
||||
SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected),
|
||||
@@ -876,15 +959,24 @@ async fn add_parts(
|
||||
} else {
|
||||
// change chat protection only when verification check passes
|
||||
if let Some(new_status) = new_status {
|
||||
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
|
||||
chat::add_info_msg(
|
||||
if chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
chat_id,
|
||||
format!("Cannot set protection: {}", e),
|
||||
sort_timestamp,
|
||||
Param::ProtectionSettingsTimestamp,
|
||||
*sent_timestamp,
|
||||
)
|
||||
.await;
|
||||
return Ok(chat_id); // do not return an error as this would result in retrying the message
|
||||
.await?
|
||||
{
|
||||
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat_id,
|
||||
format!("Cannot set protection: {}", e),
|
||||
sort_timestamp,
|
||||
)
|
||||
.await;
|
||||
return Ok(chat_id); // do not return an error as this would result in retrying the message
|
||||
}
|
||||
}
|
||||
set_better_msg(
|
||||
mime_parser,
|
||||
@@ -910,8 +1002,6 @@ async fn add_parts(
|
||||
std::cmp::max(sort_timestamp, parent_timestamp)
|
||||
});
|
||||
|
||||
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
|
||||
|
||||
// if the mime-headers should be saved, find out its size
|
||||
// (the mime-header ends with an empty line)
|
||||
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await?;
|
||||
@@ -973,7 +1063,7 @@ INSERT INTO msgs
|
||||
txt, subject, txt_raw, param,
|
||||
bytes, hidden, mime_headers, mime_in_reply_to,
|
||||
mime_references, mime_modified, error, ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
ephemeral_timestamp, download_state
|
||||
)
|
||||
VALUES (
|
||||
?, ?, ?, ?,
|
||||
@@ -982,7 +1072,7 @@ INSERT INTO msgs
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?
|
||||
?, ?
|
||||
);
|
||||
"#,
|
||||
)?;
|
||||
@@ -1017,7 +1107,9 @@ INSERT INTO msgs
|
||||
} else {
|
||||
match ephemeral_timer {
|
||||
EphemeralTimer::Disabled => 0,
|
||||
EphemeralTimer::Enabled { duration } => rcvd_timestamp + i64::from(duration),
|
||||
EphemeralTimer::Enabled { duration } => {
|
||||
rcvd_timestamp.saturating_add(duration.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1059,7 +1151,12 @@ INSERT INTO msgs
|
||||
mime_modified,
|
||||
part.error.take().unwrap_or_default(),
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp
|
||||
ephemeral_timestamp,
|
||||
if is_partial_download.is_some() {
|
||||
DownloadState::Available
|
||||
} else {
|
||||
DownloadState::Done
|
||||
},
|
||||
])?;
|
||||
let row_id = conn.last_insert_rowid();
|
||||
|
||||
@@ -1097,25 +1194,23 @@ INSERT INTO msgs
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_last_subject(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> Result<()> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
chat.param.set(
|
||||
Param::LastSubject,
|
||||
mime_parser
|
||||
.get_subject()
|
||||
.ok_or_else(|| format_err!("No subject in email"))?,
|
||||
);
|
||||
chat.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
if !is_mdn {
|
||||
update_last_subject(context, chat_id, mime_parser)
|
||||
.await
|
||||
.ok_or_log_msg(context, "Could not update LastSubject of chat");
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
// In contrast to most other update-timestamps,
|
||||
// use `sort_timestamp` instead of `sent_timestamp` for the subject-timestamp comparison.
|
||||
// This way, `LastSubject` actually refers to the most recent message _shown_ in the chat.
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::SubjectTimestamp, sort_timestamp)?
|
||||
{
|
||||
// write the last subject even if empty -
|
||||
// otherwise a reply may get an outdated subject.
|
||||
let subject = mime_parser.get_subject().unwrap_or_default();
|
||||
|
||||
chat.param.set(Param::LastSubject, subject);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(chat_id)
|
||||
@@ -1314,6 +1409,7 @@ async fn is_probably_private_reply(
|
||||
async fn create_or_lookup_group(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
sent_timestamp: i64,
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
from_id: u32,
|
||||
@@ -1323,7 +1419,6 @@ async fn create_or_lookup_group(
|
||||
let mut recreate_member_list = false;
|
||||
let mut send_EVENT_CHAT_MODIFIED = false;
|
||||
let mut X_MrAddToGrp = None;
|
||||
let mut X_MrGrpNameChanged = false;
|
||||
let mut better_msg: String = From::from("");
|
||||
|
||||
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
|
||||
@@ -1401,7 +1496,6 @@ async fn create_or_lookup_group(
|
||||
better_msg = stock_str::msg_add_member(context, &added_member, from_id).await;
|
||||
X_MrAddToGrp = Some(added_member);
|
||||
} else if let Some(old_name) = mime_parser.get_header(HeaderDef::ChatGroupNameChanged) {
|
||||
X_MrGrpNameChanged = true;
|
||||
better_msg = stock_str::msg_grp_name(
|
||||
context,
|
||||
old_name,
|
||||
@@ -1531,9 +1625,16 @@ async fn create_or_lookup_group(
|
||||
// execute group commands
|
||||
if X_MrAddToGrp.is_some() {
|
||||
recreate_member_list = true;
|
||||
} else if X_MrGrpNameChanged {
|
||||
} else if mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.is_some()
|
||||
{
|
||||
if let Some(ref grpname) = grpname {
|
||||
if grpname.len() < 200 {
|
||||
if grpname.len() < 200
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
info!(context, "updating grpname for chat {}", chat_id);
|
||||
if context
|
||||
.sql
|
||||
@@ -1555,54 +1656,69 @@ async fn create_or_lookup_group(
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
info!(context, "group-avatar change for {}", chat_id);
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
match avatar_action {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
chat.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
}
|
||||
};
|
||||
chat.update_param(context).await?;
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
if chat
|
||||
.param
|
||||
.update_timestamp(Param::AvatarTimestamp, sent_timestamp)?
|
||||
{
|
||||
match avatar_action {
|
||||
AvatarAction::Change(profile_image) => {
|
||||
chat.param.set(Param::ProfileImage, profile_image);
|
||||
}
|
||||
AvatarAction::Delete => {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
}
|
||||
};
|
||||
chat.update_param(context).await?;
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add members to group/check members
|
||||
if recreate_member_list {
|
||||
if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
// Members could have been removed while we were
|
||||
// absent. We can't use existing member list and need to
|
||||
// start from scratch.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await;
|
||||
}
|
||||
if from_id > DC_CONTACT_ID_LAST_SPECIAL
|
||||
&& !Contact::addr_equals_contact(context, &self_addr, from_id).await
|
||||
&& !chat::is_contact_in_chat(context, chat_id, from_id).await
|
||||
if chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, from_id).await;
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
|
||||
if !Contact::addr_equals_contact(context, &self_addr, to_id).await
|
||||
&& !chat::is_contact_in_chat(context, chat_id, to_id).await
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, to_id).await;
|
||||
if !chat::is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await {
|
||||
// Members could have been removed while we were
|
||||
// absent. We can't use existing member list and need to
|
||||
// start from scratch.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF).await;
|
||||
}
|
||||
if from_id > DC_CONTACT_ID_LAST_SPECIAL
|
||||
&& !Contact::addr_equals_contact(context, &self_addr, from_id).await
|
||||
&& !chat::is_contact_in_chat(context, chat_id, from_id).await
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, from_id).await;
|
||||
}
|
||||
for &to_id in to_ids.iter() {
|
||||
info!(context, "adding to={:?} to chat id={}", to_id, chat_id);
|
||||
if !Contact::addr_equals_contact(context, &self_addr, to_id).await
|
||||
&& !chat::is_contact_in_chat(context, chat_id, to_id).await
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, to_id).await;
|
||||
}
|
||||
}
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
}
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
} else if let Some(contact_id) = removed_id {
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await;
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
if chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
{
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await;
|
||||
send_EVENT_CHAT_MODIFIED = true;
|
||||
}
|
||||
}
|
||||
|
||||
if send_EVENT_CHAT_MODIFIED {
|
||||
@@ -1665,14 +1781,18 @@ async fn create_or_lookup_mailinglist(
|
||||
}
|
||||
|
||||
// if we do not have a name yet and `From` indicates, that this is a notification list,
|
||||
// a usable name is often in the `From` header.
|
||||
// this pattern was seen for parcel service notifications
|
||||
// and is similar to mailchimp above, however, with weaker conditions.
|
||||
// a usable name is often in the `From` header (seen for several parcel service notifications).
|
||||
// same, if we do not have a name yet and `List-Id` has a known suffix (`.xt.local`)
|
||||
//
|
||||
// this pattern is similar to mailchimp above, however,
|
||||
// with weaker conditions and does not overwrite existing names.
|
||||
if name.is_empty() {
|
||||
if let Some(from) = mime_parser.from.first() {
|
||||
if from.addr.contains("noreply")
|
||||
|| from.addr.contains("no-reply")
|
||||
|| from.addr.starts_with("notifications@")
|
||||
|| from.addr.starts_with("newsletter@")
|
||||
|| listid.ends_with(".xt.local")
|
||||
{
|
||||
if let Some(display_name) = &from.display_name {
|
||||
name = display_name.clone();
|
||||
@@ -1682,8 +1802,16 @@ async fn create_or_lookup_mailinglist(
|
||||
}
|
||||
|
||||
// as a last resort, use the ListId as the name
|
||||
// but strip some known, long hash prefixes
|
||||
if name.is_empty() {
|
||||
name = listid.clone();
|
||||
// 51231231231231231231231232869f58.xing.com -> xing.com
|
||||
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
|
||||
if let Some(cap) = PREFIX_32_CHARS_HEX.captures(&listid) {
|
||||
name = cap[2].to_string();
|
||||
} else {
|
||||
name = listid.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if allow_creation {
|
||||
@@ -2739,6 +2867,18 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_ndn_testrun_2() {
|
||||
test_parse_ndn(
|
||||
"alice@example.org",
|
||||
"bob@example.org",
|
||||
"Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org",
|
||||
include_bytes!("../test-data/message/testrun_ndn_2.eml"),
|
||||
Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<bob@example.org>: Host or domain name not found. Name service error for\n name=echedelyr.tk type=AAAA: Host not found"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// ndn = Non Delivery Notification
|
||||
async fn test_parse_ndn(
|
||||
self_addr: &str,
|
||||
@@ -3273,6 +3413,86 @@ mod tests {
|
||||
assert_eq!(chat.name, "DPD");
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_xt_local_mailing_list() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.grpid, "96540.xt.local");
|
||||
assert_eq!(chat.name, "Microsoft Store");
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"),
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.grpid, "121231234.xt.local");
|
||||
assert_eq!(chat.name, "DER SPIEGEL Kundenservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_xing_mailing_list() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
include_bytes!("../test-data/message/mailinglist_xing.eml"),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.subject, "Kennst Du Dr. Mabuse?");
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com");
|
||||
assert_eq!(chat.name, "xing.com");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ttline_mailing_list() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config(Config::ShowEmails, Some("2")).await?;
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
include_bytes!("../test-data/message/mailinglist_ttline.eml"),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.subject, "Unsere Sommerangebote an Bord ⚓");
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.grpid, "39123123-1BBQXPY.t.ttline.com");
|
||||
assert_eq!(chat.name, "TT-Line - Die Schwedenfähren");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_mailing_list_with_mimepart_footer() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
346
src/download.rs
Normal file
346
src/download.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
//! # Download large messages manually.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::job::{self, Action, Job, Status};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::param::Params;
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
use std::cmp::max;
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// need to be downloaded completely to handle them correctly,
|
||||
/// eg. to assign them to the correct chat.
|
||||
/// As these messages are typically small,
|
||||
/// they're catched by `MIN_DOWNLOAD_LIMIT`.
|
||||
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
/// the user might have no chance to actually download that message.
|
||||
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
|
||||
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
FromSql,
|
||||
ToSql,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum DownloadState {
|
||||
Done = 0,
|
||||
Available = 10,
|
||||
Failure = 20,
|
||||
InProgress = 1000,
|
||||
}
|
||||
|
||||
impl Default for DownloadState {
|
||||
fn default() -> Self {
|
||||
DownloadState::Done
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
// Returns validated download limit or `None` for "no limit".
|
||||
pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {
|
||||
let download_limit = self.get_config_int(Config::DownloadLimit).await?;
|
||||
if download_limit <= 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
/// Schedules full message download for partially downloaded message.
|
||||
pub async fn download_full(self, context: &Context) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
match msg.download_state() {
|
||||
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
|
||||
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
|
||||
DownloadState::Available | DownloadState::Failure => {
|
||||
self.update_download_state(context, DownloadState::InProgress)
|
||||
.await?;
|
||||
job::add(
|
||||
context,
|
||||
Job::new(Action::DownloadMsg, self.to_u32(), Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_download_state(
|
||||
self,
|
||||
context: &Context,
|
||||
download_state: DownloadState,
|
||||
) -> Result<()> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET download_state=? WHERE id=?;",
|
||||
paramsv![download_state, self],
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: self,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Returns the download state of the message.
|
||||
pub fn download_state(&self) -> DownloadState {
|
||||
self.download_state
|
||||
}
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// Actually download a message.
|
||||
/// Called in response to `Action::DownloadMsg`.
|
||||
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "download: could not connect: {:?}", err);
|
||||
return Status::RetryNow;
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
|
||||
let server_folder = msg.server_folder.unwrap_or_default();
|
||||
match imap
|
||||
.fetch_single_msg(context, &server_folder, msg.server_uid)
|
||||
.await
|
||||
{
|
||||
ImapActionResult::RetryLater | ImapActionResult::Failed => {
|
||||
job_try!(
|
||||
msg.id
|
||||
.update_download_state(context, DownloadState::Failure)
|
||||
.await
|
||||
);
|
||||
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
|
||||
}
|
||||
ImapActionResult::Success | ImapActionResult::AlreadyDone => {
|
||||
// update_download_state() not needed as receive_imf() already
|
||||
// set the state and emitted the event.
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
/// Download a single message and pipe it to receive_imf().
|
||||
///
|
||||
/// receive_imf() is not directly aware that this is a result of a call to download_msg(),
|
||||
/// however, implicitly knows that as the existing message is flagged as being partly.
|
||||
async fn fetch_single_msg(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
) -> ImapActionResult {
|
||||
if let Some(imapresult) = self
|
||||
.prepare_imap_operation_on_msg(context, folder, uid)
|
||||
.await
|
||||
{
|
||||
return imapresult;
|
||||
}
|
||||
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
let (_, error_cnt) = self
|
||||
.fetch_many_msgs(context, folder, vec![uid], false, false)
|
||||
.await;
|
||||
if error_cnt > 0 {
|
||||
return ImapActionResult::Failed;
|
||||
}
|
||||
|
||||
ImapActionResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeMessage {
|
||||
/// Creates a placeholder part and add that to `parts`.
|
||||
///
|
||||
/// To create the placeholder, only the outermost header can be used,
|
||||
/// the mime-structure itself is not available.
|
||||
///
|
||||
/// The placeholder part currently contains a text with size and availability of the message;
|
||||
/// in the future, we may do more advanced things as previews here.
|
||||
pub(crate) async fn create_stub_from_partial_download(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
org_bytes: u32,
|
||||
) -> Result<()> {
|
||||
let mut text = format!(
|
||||
"[{}]",
|
||||
stock_str::partial_download_msg_body(context, org_bytes).await
|
||||
);
|
||||
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
|
||||
let until = stock_str::download_availability(
|
||||
context,
|
||||
time() + max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
)
|
||||
.await;
|
||||
text += format!(" [{}]", until).as_str();
|
||||
};
|
||||
|
||||
info!(context, "Partial download: {}", text);
|
||||
|
||||
self.parts.push(Part {
|
||||
typ: Viewtype::Text,
|
||||
msg: text,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::send_msg;
|
||||
use crate::constants::Viewtype;
|
||||
use crate::dc_receive_imf::dc_receive_imf_inner;
|
||||
use crate::test_utils::TestContext;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(DownloadState::Done, DownloadState::default());
|
||||
assert_eq!(DownloadState::Done, DownloadState::from_i32(0).unwrap());
|
||||
assert_eq!(
|
||||
DownloadState::Available,
|
||||
DownloadState::from_i32(10).unwrap()
|
||||
);
|
||||
assert_eq!(DownloadState::Failure, DownloadState::from_i32(20).unwrap());
|
||||
assert_eq!(
|
||||
DownloadState::InProgress,
|
||||
DownloadState::from_i32(1000).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_download_limit() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
|
||||
t.set_config(Config::DownloadLimit, Some("200000")).await?;
|
||||
assert_eq!(t.download_limit().await?, Some(200000));
|
||||
|
||||
t.set_config(Config::DownloadLimit, Some("20000")).await?;
|
||||
assert_eq!(t.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
|
||||
t.set_config(Config::DownloadLimit, None).await?;
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
|
||||
for val in &["0", "-1", "-100", "", "foo"] {
|
||||
t.set_config(Config::DownloadLimit, Some(val)).await?;
|
||||
assert_eq!(t.download_limit().await?, None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_update_download_state() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("Hi Bob".to_owned()));
|
||||
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
|
||||
for s in &[
|
||||
DownloadState::Available,
|
||||
DownloadState::InProgress,
|
||||
DownloadState::Failure,
|
||||
DownloadState::Done,
|
||||
] {
|
||||
msg_id.update_download_state(&t, *s).await?;
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
assert_eq!(msg.download_state(), *s);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_partial_receive_imf() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let header =
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: bob@example.com\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <Mr.12345678901@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\
|
||||
Content-Type: text/plain";
|
||||
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
header.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert!(msg
|
||||
.get_text()
|
||||
.unwrap()
|
||||
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
|
||||
|
||||
dc_receive_imf_inner(
|
||||
&t,
|
||||
format!("{}\n\n100k text...", header).as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||
assert_eq!(msg.get_subject(), "foo");
|
||||
assert_eq!(msg.get_text(), Some("100k text...".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
103
src/ephemeral.rs
103
src/ephemeral.rs
@@ -71,11 +71,13 @@ use crate::constants::{
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::job;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
@@ -279,7 +281,7 @@ impl MsgId {
|
||||
/// Starts ephemeral message timer for the message if it is not started yet.
|
||||
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
|
||||
let ephemeral_timestamp = time() + i64::from(duration);
|
||||
let ephemeral_timestamp = time().saturating_add(duration.into());
|
||||
|
||||
context
|
||||
.sql
|
||||
@@ -416,24 +418,18 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
let context1 = context.clone();
|
||||
let ephemeral_task = task::spawn(async move {
|
||||
async_std::task::sleep(duration).await;
|
||||
emit_event!(
|
||||
context1,
|
||||
EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
context1.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
});
|
||||
*context.ephemeral_task.write().await = Some(ephemeral_task);
|
||||
} else {
|
||||
// Emit event immediately
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0)
|
||||
}
|
||||
);
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,23 +441,32 @@ pub async fn schedule_ephemeral_task(context: &Context) {
|
||||
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
|
||||
let now = time();
|
||||
|
||||
let threshold_timestamp = match context.get_config_delete_server_after().await? {
|
||||
None => 0,
|
||||
Some(delete_server_after) => now - delete_server_after,
|
||||
};
|
||||
let (threshold_timestamp, threshold_timestamp_extended) =
|
||||
match context.get_config_delete_server_after().await? {
|
||||
None => (0, 0),
|
||||
Some(delete_server_after) => (
|
||||
now - delete_server_after,
|
||||
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
|
||||
),
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs \
|
||||
WHERE ( \
|
||||
timestamp < ? \
|
||||
((download_state = 0 AND timestamp < ?) OR (download_state != 0 AND timestamp < ?)) \
|
||||
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
|
||||
) \
|
||||
AND server_uid != 0 \
|
||||
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
|
||||
LIMIT 1",
|
||||
paramsv![threshold_timestamp, now, job::Action::DeleteMsgOnImap],
|
||||
paramsv![
|
||||
threshold_timestamp,
|
||||
threshold_timestamp_extended,
|
||||
now,
|
||||
job::Action::DeleteMsgOnImap
|
||||
],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
@@ -506,6 +511,8 @@ mod tests {
|
||||
use async_std::task::sleep;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::download::DownloadState;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::{
|
||||
chat::{self, Chat, ChatItem},
|
||||
@@ -777,4 +784,58 @@ mod tests {
|
||||
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_imap_deletion_msgid() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
let now = time();
|
||||
for (id, timestamp, ephemeral_timestamp) in &[
|
||||
(900, now - 2 * HOUR, 0),
|
||||
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
|
||||
(1010, now - 23 * HOUR, 0),
|
||||
(1020, now - 21 * HOUR, 0),
|
||||
(1030, now - 19 * HOUR, 0),
|
||||
(2000, now - 18 * HOUR, now - HOUR),
|
||||
(2020, now - 17 * HOUR, now + HOUR),
|
||||
] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, server_uid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, id, timestamp, ephemeral_timestamp],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(2000)));
|
||||
|
||||
MsgId::new(2000).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000)));
|
||||
|
||||
MsgId::new(1000)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000))); // delete downloadable anyway
|
||||
|
||||
MsgId::new(1000).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
|
||||
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1010)));
|
||||
|
||||
MsgId::new(1010)
|
||||
.update_download_state(&t, DownloadState::Available)
|
||||
.await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None); // keep downloadable for now
|
||||
|
||||
MsgId::new(1010).delete_from_db(&t).await?;
|
||||
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
349
src/imap.rs
349
src/imap.rs
@@ -65,7 +65,7 @@ pub enum ImapActionResult {
|
||||
/// - Chat-Version to check if a message is a chat message
|
||||
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
||||
/// not necessarily sent by Delta Chat.
|
||||
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
||||
const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
||||
MESSAGE-ID \
|
||||
FROM \
|
||||
IN-REPLY-TO REFERENCES \
|
||||
@@ -81,14 +81,14 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
|
||||
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
|
||||
)])";
|
||||
const JUST_UID: &str = "(UID)";
|
||||
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
|
||||
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
|
||||
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Imap {
|
||||
idle_interrupt: Receiver<InterruptInfo>,
|
||||
config: ImapConfig,
|
||||
session: Option<Session>,
|
||||
connected: bool,
|
||||
interrupt: Option<stop_token::StopSource>,
|
||||
should_reconnect: bool,
|
||||
login_failed_once: bool,
|
||||
@@ -200,7 +200,6 @@ impl Imap {
|
||||
idle_interrupt,
|
||||
config,
|
||||
session: None,
|
||||
connected: false,
|
||||
interrupt: None,
|
||||
should_reconnect: false,
|
||||
login_failed_once: false,
|
||||
@@ -228,7 +227,11 @@ impl Imap {
|
||||
param.socks5_config.clone(),
|
||||
¶m.addr,
|
||||
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
param.provider.map_or(false, |provider| provider.strict_tls),
|
||||
param
|
||||
.provider
|
||||
.map_or(param.socks5_config.is_some(), |provider| {
|
||||
provider.strict_tls
|
||||
}),
|
||||
idle_interrupt,
|
||||
)
|
||||
.await?;
|
||||
@@ -250,7 +253,7 @@ impl Imap {
|
||||
if self.should_reconnect() {
|
||||
self.disconnect(context).await;
|
||||
self.should_reconnect = false;
|
||||
} else if self.is_connected() {
|
||||
} else if self.session.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -343,13 +346,12 @@ impl Imap {
|
||||
match login_res {
|
||||
Ok(session) => {
|
||||
// needs to be set here to ensure it is set on reconnects.
|
||||
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))
|
||||
);
|
||||
context.emit_event(EventType::ImapConnected(format!(
|
||||
"IMAP-LOGIN as {}",
|
||||
self.config.lp.user
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -438,16 +440,11 @@ impl Imap {
|
||||
warn!(context, "failed to logout: {:?}", err);
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
pub fn should_reconnect(&self) -> bool {
|
||||
self.should_reconnect
|
||||
}
|
||||
@@ -583,7 +580,7 @@ impl Imap {
|
||||
folder, old_uid_next, uid_next, new_uid_validity,
|
||||
);
|
||||
set_uid_next(context, folder, uid_next).await?;
|
||||
job::schedule_resync(context).await;
|
||||
job::schedule_resync(context).await?;
|
||||
}
|
||||
uid_next != old_uid_next // If uid_next changed, there are new emails
|
||||
} else {
|
||||
@@ -636,7 +633,7 @@ impl Imap {
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
set_uidvalidity(context, folder, new_uid_validity).await?;
|
||||
if old_uid_validity != 0 || old_uid_next != 0 {
|
||||
job::schedule_resync(context).await;
|
||||
job::schedule_resync(context).await?;
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
@@ -658,6 +655,7 @@ impl Imap {
|
||||
) -> Result<bool> {
|
||||
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default();
|
||||
let download_limit = context.download_limit().await?;
|
||||
|
||||
let new_emails = self
|
||||
.select_with_uidvalidity(context, folder.as_ref())
|
||||
@@ -679,7 +677,8 @@ impl Imap {
|
||||
let folder: &str = folder.as_ref();
|
||||
|
||||
let mut read_errors = 0;
|
||||
let mut uids = Vec::with_capacity(msgs.len());
|
||||
let mut uids_fetch_fully = Vec::with_capacity(msgs.len());
|
||||
let mut uids_fetch_partially = Vec::with_capacity(msgs.len());
|
||||
let mut largest_uid_skipped = None;
|
||||
|
||||
for (current_uid, msg) in msgs.into_iter() {
|
||||
@@ -706,7 +705,16 @@ impl Imap {
|
||||
)
|
||||
.await
|
||||
{
|
||||
uids.push(current_uid);
|
||||
match download_limit {
|
||||
Some(download_limit) => {
|
||||
if msg.size.unwrap_or_default() > download_limit {
|
||||
uids_fetch_partially.push(current_uid);
|
||||
} else {
|
||||
uids_fetch_fully.push(current_uid)
|
||||
}
|
||||
}
|
||||
None => uids_fetch_fully.push(current_uid),
|
||||
}
|
||||
} else if read_errors == 0 {
|
||||
// If there were errors (`read_errors != 0`), stop updating largest_uid_skipped so that uid_next will
|
||||
// not be updated and we will retry prefetching next time
|
||||
@@ -714,12 +722,29 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
if !uids.is_empty() {
|
||||
if !uids_fetch_fully.is_empty() || !uids_fetch_partially.is_empty() {
|
||||
self.connectivity.set_working(context).await;
|
||||
}
|
||||
|
||||
let (largest_uid_processed, error_cnt) = self
|
||||
.fetch_many_msgs(context, folder, uids, fetch_existing_msgs)
|
||||
let (largest_uid_fully_fetched, error_cnt) = self
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uids_fetch_fully,
|
||||
false,
|
||||
fetch_existing_msgs,
|
||||
)
|
||||
.await;
|
||||
read_errors += error_cnt;
|
||||
|
||||
let (largest_uid_partially_fetched, error_cnt) = self
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uids_fetch_partially,
|
||||
true,
|
||||
fetch_existing_msgs,
|
||||
)
|
||||
.await;
|
||||
read_errors += error_cnt;
|
||||
|
||||
@@ -730,7 +755,10 @@ impl Imap {
|
||||
// So: Update the uid_next to the largest uid that did NOT recoverably fail. Not perfect because if there was
|
||||
// another message afterwards that succeeded, we will not retry. The upside is that we will not retry an infinite amount of times.
|
||||
let largest_uid_without_errors = max(
|
||||
largest_uid_processed.unwrap_or(0),
|
||||
max(
|
||||
largest_uid_fully_fetched.unwrap_or(0),
|
||||
largest_uid_partially_fetched.unwrap_or(0),
|
||||
),
|
||||
largest_uid_skipped.unwrap_or(0),
|
||||
);
|
||||
let new_uid_next = largest_uid_without_errors + 1;
|
||||
@@ -867,30 +895,25 @@ impl Imap {
|
||||
/// Fetches a list of messages by server UID.
|
||||
///
|
||||
/// Returns the last uid fetch successfully and an error count.
|
||||
async fn fetch_many_msgs(
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
server_uids: Vec<u32>,
|
||||
fetch_partially: bool,
|
||||
fetching_existing_messages: bool,
|
||||
) -> (Option<u32>, usize) {
|
||||
if server_uids.is_empty() {
|
||||
return (None, 0);
|
||||
}
|
||||
|
||||
if !self.is_connected() {
|
||||
warn!(context, "Not connected");
|
||||
return (None, server_uids.len());
|
||||
}
|
||||
|
||||
if self.session.is_none() {
|
||||
// we could not get a valid imap session, this should be retried
|
||||
self.trigger_reconnect(context).await;
|
||||
warn!(context, "Could not get IMAP session");
|
||||
return (None, server_uids.len());
|
||||
}
|
||||
|
||||
let session = self.session.as_mut().unwrap();
|
||||
let session = match self.session.as_mut() {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!(context, "Not connected");
|
||||
return (None, server_uids.len());
|
||||
}
|
||||
};
|
||||
|
||||
let sets = build_sequence_sets(server_uids.clone());
|
||||
let mut read_errors = 0;
|
||||
@@ -898,7 +921,17 @@ impl Imap {
|
||||
let mut last_uid = None;
|
||||
|
||||
for set in sets.iter() {
|
||||
let mut msgs = match session.uid_fetch(&set, BODY_FLAGS).await {
|
||||
let mut msgs = match session
|
||||
.uid_fetch(
|
||||
&set,
|
||||
if fetch_partially {
|
||||
BODY_PARTIAL
|
||||
} else {
|
||||
BODY_FULL
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(msgs) => msgs,
|
||||
Err(err) => {
|
||||
// TODO: maybe differentiate between IO and input/parsing problems
|
||||
@@ -933,7 +966,13 @@ impl Imap {
|
||||
count += 1;
|
||||
|
||||
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
|
||||
if is_deleted || msg.body().is_none() {
|
||||
let (body, partial) = if fetch_partially {
|
||||
(msg.header(), msg.size) // `BODY.PEEK[HEADER]` goes to header() ...
|
||||
} else {
|
||||
(msg.body(), None) // ... while `BODY.PEEK[]` goes to body() - and includes header()
|
||||
};
|
||||
|
||||
if is_deleted || body.is_none() {
|
||||
info!(
|
||||
context,
|
||||
"Not processing deleted or empty msg {}", server_uid
|
||||
@@ -947,7 +986,7 @@ impl Imap {
|
||||
let folder = folder.clone();
|
||||
|
||||
// safe, as we checked above that there is a body.
|
||||
let body = msg.body().unwrap();
|
||||
let body = body.unwrap();
|
||||
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
|
||||
|
||||
match dc_receive_imf_inner(
|
||||
@@ -956,6 +995,7 @@ impl Imap {
|
||||
&folder,
|
||||
server_uid,
|
||||
is_seen,
|
||||
partial,
|
||||
fetching_existing_messages,
|
||||
)
|
||||
.await
|
||||
@@ -1011,13 +1051,10 @@ impl Imap {
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
match session.uid_mv(&set, &dest_folder).await {
|
||||
Ok(_) => {
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} moved to {}",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} moved to {}",
|
||||
display_folder_id, dest_folder
|
||||
)));
|
||||
return ImapActionResult::Success;
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1055,23 +1092,17 @@ impl Imap {
|
||||
|
||||
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
|
||||
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete FAILED)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete FAILED)",
|
||||
display_folder_id, dest_folder
|
||||
)));
|
||||
ImapActionResult::Failed
|
||||
} else {
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete successfull)",
|
||||
display_folder_id, dest_folder
|
||||
))
|
||||
);
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP Message {} copied to {} (delete successfull)",
|
||||
display_folder_id, dest_folder
|
||||
)));
|
||||
ImapActionResult::Success
|
||||
}
|
||||
}
|
||||
@@ -1128,7 +1159,7 @@ impl Imap {
|
||||
if uid == 0 {
|
||||
return Some(ImapActionResult::RetryLater);
|
||||
}
|
||||
if !self.is_connected() {
|
||||
if self.session.is_none() {
|
||||
// currently jobs are only performed on the INBOX thread
|
||||
// TODO: make INBOX/SENT/MVBOX perform the jobs on their
|
||||
// respective folders to avoid select_folder network traffic
|
||||
@@ -1265,13 +1296,10 @@ impl Imap {
|
||||
);
|
||||
ImapActionResult::RetryLater
|
||||
} else {
|
||||
emit_event!(
|
||||
context,
|
||||
EventType::ImapMessageDeleted(format!(
|
||||
"IMAP Message {} marked as deleted [{}]",
|
||||
display_imap_id, message_id
|
||||
))
|
||||
);
|
||||
context.emit_event(EventType::ImapMessageDeleted(format!(
|
||||
"IMAP Message {} marked as deleted [{}]",
|
||||
display_imap_id, message_id
|
||||
)));
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
ImapActionResult::Success
|
||||
}
|
||||
@@ -1291,115 +1319,114 @@ impl Imap {
|
||||
}
|
||||
|
||||
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> {
|
||||
if !self.is_connected() {
|
||||
bail!("IMAP No Connection established");
|
||||
}
|
||||
let session = match self.session {
|
||||
Some(ref mut session) => session,
|
||||
None => bail!("no IMAP connection established"),
|
||||
};
|
||||
|
||||
if let Some(ref mut session) = &mut self.session {
|
||||
let mut folders = match session.list(Some(""), Some("*")).await {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
bail!("list_folders failed: {}", err);
|
||||
}
|
||||
};
|
||||
let mut folders = match session.list(Some(""), Some("*")).await {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
bail!("list_folders failed: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let mut delimiter = ".".to_string();
|
||||
let mut delimiter_is_default = true;
|
||||
let mut mvbox_folder = None;
|
||||
let mut folder_configs = BTreeMap::new();
|
||||
let mut fallback_folder = get_fallback_folder(&delimiter);
|
||||
let mut delimiter = ".".to_string();
|
||||
let mut delimiter_is_default = true;
|
||||
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 {
|
||||
let folder = folder?;
|
||||
info!(context, "Scanning folder: {:?}", folder);
|
||||
while let Some(folder) = folders.next().await {
|
||||
let folder = folder?;
|
||||
info!(context, "Scanning folder: {:?}", folder);
|
||||
|
||||
// Update the delimiter iff there is a different one, but only once.
|
||||
if let Some(d) = folder.delimiter() {
|
||||
if delimiter_is_default && !d.is_empty() && delimiter != d {
|
||||
delimiter = d.to_string();
|
||||
fallback_folder = get_fallback_folder(&delimiter);
|
||||
delimiter_is_default = false;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
mvbox_folder = Some(folder.name().to_string());
|
||||
} else if folder.name() == fallback_folder {
|
||||
// only set if 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());
|
||||
// Update the delimiter iff there is a different one, but only once.
|
||||
if let Some(d) = folder.delimiter() {
|
||||
if delimiter_is_default && !d.is_empty() && delimiter != d {
|
||||
delimiter = d.to_string();
|
||||
fallback_folder = get_fallback_folder(&delimiter);
|
||||
delimiter_is_default = false;
|
||||
}
|
||||
}
|
||||
drop(folders);
|
||||
|
||||
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
|
||||
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
|
||||
mvbox_folder = Some(folder.name().to_string());
|
||||
} else if folder.name() == fallback_folder {
|
||||
// only set if 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());
|
||||
}
|
||||
}
|
||||
drop(folders);
|
||||
|
||||
if mvbox_folder.is_none() && create_mvbox {
|
||||
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
|
||||
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
|
||||
|
||||
match session.create("DeltaChat").await {
|
||||
Ok(_) => {
|
||||
mvbox_folder = Some("DeltaChat".into());
|
||||
info!(context, "MVBOX-folder created.",);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})",
|
||||
err
|
||||
);
|
||||
if mvbox_folder.is_none() && create_mvbox {
|
||||
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
|
||||
|
||||
match session.create(&fallback_folder).await {
|
||||
Ok(_) => {
|
||||
mvbox_folder = Some(fallback_folder);
|
||||
info!(
|
||||
context,
|
||||
"MVBOX-folder created as INBOX subfolder. ({})", err
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder. ({})", err);
|
||||
}
|
||||
match session.create("DeltaChat").await {
|
||||
Ok(_) => {
|
||||
mvbox_folder = Some("DeltaChat".into());
|
||||
info!(context, "MVBOX-folder created.",);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})", err
|
||||
);
|
||||
|
||||
match session.create(&fallback_folder).await {
|
||||
Ok(_) => {
|
||||
mvbox_folder = Some(fallback_folder);
|
||||
info!(
|
||||
context,
|
||||
"MVBOX-folder created as INBOX subfolder. ({})", err
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder. ({})", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// SUBSCRIBE is needed to make the folder visible to the LSUB command
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// SUBSCRIBE is needed to make the folder visible to the LSUB command
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
context
|
||||
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
|
||||
.await?;
|
||||
if let Some(ref mvbox_folder) = mvbox_folder {
|
||||
context
|
||||
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
|
||||
.await?;
|
||||
if let Some(ref mvbox_folder) = mvbox_folder {
|
||||
context
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
|
||||
.await?;
|
||||
}
|
||||
for (config, name) in folder_configs {
|
||||
context.set_config(config, Some(&name)).await?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
|
||||
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
|
||||
.await?;
|
||||
}
|
||||
for (config, name) in folder_configs {
|
||||
context.set_config(config, Some(&name)).await?;
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
|
||||
.await?;
|
||||
|
||||
info!(context, "FINISHED configuring IMAP-folders.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg = Message::default();
|
||||
msg.viewtype = Viewtype::File;
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
|
||||
msg.subject = stock_str::ac_setup_msg_subject(context).await;
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
@@ -759,7 +759,7 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
|
||||
let progress = 1000 * written_files / count;
|
||||
if progress != last_progress && progress > 10 && progress < 1000 {
|
||||
// We already emitted ImexProgress(10) above
|
||||
emit_event!(context, EventType::ImexProgress(progress));
|
||||
context.emit_event(EventType::ImexProgress(progress));
|
||||
last_progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
107
src/job.rs
107
src/job.rs
@@ -2,12 +2,11 @@
|
||||
//!
|
||||
//! This module implements a job queue maintained in the SQLite database
|
||||
//! and job types.
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::{fmt, time::Duration};
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
|
||||
use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use async_std::task::sleep;
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rand::{thread_rng, Rng};
|
||||
@@ -103,6 +102,12 @@ pub enum Action {
|
||||
MoveMsg = 200,
|
||||
DeleteMsgOnImap = 210,
|
||||
|
||||
// This job will download partially downloaded messages completely
|
||||
// and is added when download_full() is called.
|
||||
// Most messages are downloaded automatically on fetch
|
||||
// and do not go through this job.
|
||||
DownloadMsg = 250,
|
||||
|
||||
// UID synchronization is high-priority to make sure correct UIDs
|
||||
// are used by message moving/deletion.
|
||||
ResyncFolders = 300,
|
||||
@@ -134,6 +139,7 @@ impl From<Action> for Thread {
|
||||
MarkseenMsgOnImap => Thread::Imap,
|
||||
MoveMsg => Thread::Imap,
|
||||
UpdateRecentQuota => Thread::Imap,
|
||||
DownloadMsg => Thread::Imap,
|
||||
|
||||
MaybeSendLocations => Thread::Smtp,
|
||||
MaybeSendLocationsEnded => Thread::Smtp,
|
||||
@@ -815,12 +821,12 @@ impl Job {
|
||||
}
|
||||
|
||||
/// Delete all pending jobs with the given action.
|
||||
pub async fn kill_action(context: &Context, action: Action) -> bool {
|
||||
pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
|
||||
.await
|
||||
.is_ok()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
@@ -1156,6 +1162,7 @@ async fn perform_job_action(
|
||||
Ok(status) => status,
|
||||
Err(err) => Status::Finished(Err(err)),
|
||||
},
|
||||
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
|
||||
};
|
||||
|
||||
info!(context, "Finished immediate try {} of job {}", tries, job);
|
||||
@@ -1183,13 +1190,14 @@ async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(context: &Context) {
|
||||
kill_action(context, Action::ResyncFolders).await;
|
||||
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
|
||||
kill_action(context, Action::ResyncFolders).await?;
|
||||
add(
|
||||
context,
|
||||
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a job.
|
||||
@@ -1219,7 +1227,8 @@ pub async fn add(context: &Context, job: Job) {
|
||||
| Action::MarkseenMsgOnImap
|
||||
| Action::FetchExistingMsgs
|
||||
| Action::MoveMsg
|
||||
| Action::UpdateRecentQuota => {
|
||||
| Action::UpdateRecentQuota
|
||||
| Action::DownloadMsg => {
|
||||
info!(context, "interrupt: imap");
|
||||
context
|
||||
.interrupt_inbox(InterruptInfo::new(false, None))
|
||||
@@ -1238,21 +1247,15 @@ pub async fn add(context: &Context, job: Job) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_housekeeping_job(context: &Context) -> Option<Job> {
|
||||
let last_time = match context.get_config_i64(Config::LastHousekeeping).await {
|
||||
Ok(last_time) => last_time,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to load housekeeping config: {:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
async fn load_housekeeping_job(context: &Context) -> Result<Option<Job>> {
|
||||
let last_time = context.get_config_i64(Config::LastHousekeeping).await?;
|
||||
|
||||
let next_time = last_time + (60 * 60 * 24);
|
||||
if next_time <= time() {
|
||||
kill_action(context, Action::Housekeeping).await;
|
||||
Some(Job::new(Action::Housekeeping, 0, Params::new(), 0))
|
||||
kill_action(context, Action::Housekeeping).await?;
|
||||
Ok(Some(Job::new(Action::Housekeeping, 0, Params::new(), 0)))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1266,20 +1269,9 @@ pub(crate) async fn load_next(
|
||||
context: &Context,
|
||||
thread: Thread,
|
||||
info: &InterruptInfo,
|
||||
) -> Option<Job> {
|
||||
) -> Result<Option<Job>> {
|
||||
info!(context, "loading job for {}-thread", thread);
|
||||
|
||||
while !context.sql.is_open().await {
|
||||
// The db is closed, which means that this thread should not be running.
|
||||
// Wait until the db is re-opened (if we returned None, this thread might do further damage)
|
||||
warn!(
|
||||
context,
|
||||
"{}: load_next() was called but the db was not opened, THIS SHOULD NOT HAPPEN. Waiting...",
|
||||
thread
|
||||
);
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
let query;
|
||||
let params;
|
||||
let t = time();
|
||||
@@ -1346,51 +1338,38 @@ LIMIT 1;
|
||||
info!(context, "cleaning up job, because of {}", err);
|
||||
|
||||
// TODO: improve by only doing a single query
|
||||
match context
|
||||
let id = context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.await
|
||||
{
|
||||
Ok(id) => {
|
||||
if let Err(err) = context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.await
|
||||
{
|
||||
warn!(context, "failed to delete job {}: {:?}", id, err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "failed to retrieve invalid job from DB: {}", err);
|
||||
break None;
|
||||
}
|
||||
}
|
||||
.context("Failed to retrieve invalid job ID from the database")?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.await
|
||||
.with_context(|| format!("Failed to delete invalid job {}", id))?;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match thread {
|
||||
Thread::Unknown => {
|
||||
error!(context, "unknown thread for job");
|
||||
None
|
||||
bail!("unknown thread for job")
|
||||
}
|
||||
Thread::Imap => {
|
||||
if let Some(job) = job {
|
||||
if job.action < Action::DeleteMsgOnImap {
|
||||
load_imap_deletion_job(context)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.or(Some(job))
|
||||
Ok(load_imap_deletion_job(context).await?.or(Some(job)))
|
||||
} else {
|
||||
Some(job)
|
||||
Ok(Some(job))
|
||||
}
|
||||
} else if let Some(job) = load_imap_deletion_job(context).await.unwrap_or_default() {
|
||||
Some(job)
|
||||
} else if let Some(job) = load_imap_deletion_job(context).await? {
|
||||
Ok(Some(job))
|
||||
} else {
|
||||
load_housekeeping_job(context).await
|
||||
Ok(load_housekeeping_job(context).await?)
|
||||
}
|
||||
}
|
||||
Thread::Smtp => job,
|
||||
Thread::Smtp => Ok(job),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1422,7 +1401,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_next_job_two() {
|
||||
async fn test_load_next_job_two() -> Result<()> {
|
||||
// We want to ensure that loading jobs skips over jobs which
|
||||
// fails to load from the database instead of failing to load
|
||||
// all jobs.
|
||||
@@ -1433,7 +1412,7 @@ mod tests {
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
|
||||
assert_eq!(jobs.unwrap().action, Action::Housekeeping);
|
||||
|
||||
@@ -1443,12 +1422,13 @@ mod tests {
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_load_next_job_one() {
|
||||
async fn test_load_next_job_one() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
insert_job(&t, 1, true).await;
|
||||
@@ -1458,7 +1438,8 @@ mod tests {
|
||||
Thread::from(Action::MoveMsg),
|
||||
&InterruptInfo::new(false, None),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
assert!(jobs.is_some());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ mod configure;
|
||||
pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
pub mod download;
|
||||
mod e2ee;
|
||||
pub mod ephemeral;
|
||||
mod imap;
|
||||
@@ -83,11 +84,13 @@ mod simplify;
|
||||
mod smtp;
|
||||
pub mod stock_str;
|
||||
mod token;
|
||||
mod update_helper;
|
||||
#[macro_use]
|
||||
mod dehtml;
|
||||
mod color;
|
||||
pub mod html;
|
||||
pub mod plaintext;
|
||||
pub mod summary;
|
||||
|
||||
pub mod dc_receive_imf;
|
||||
pub mod dc_tools;
|
||||
|
||||
163
src/location.rs
163
src/location.rs
@@ -1,7 +1,7 @@
|
||||
//! Location handling.
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{ensure, Error};
|
||||
use anyhow::{ensure, Result};
|
||||
use bitflags::bitflags;
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Kml {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self, Error> {
|
||||
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self> {
|
||||
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
|
||||
|
||||
let mut reader = quick_xml::Reader::from_reader(to_parse);
|
||||
@@ -191,54 +191,55 @@ impl Kml {
|
||||
}
|
||||
|
||||
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
|
||||
pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
|
||||
pub async fn send_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
seconds: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(seconds >= 0);
|
||||
ensure!(!chat_id.is_special());
|
||||
let now = time();
|
||||
if !(seconds < 0 || chat_id.is_special()) {
|
||||
let is_sending_locations_before =
|
||||
is_sending_locations_to_chat(context, Some(chat_id)).await;
|
||||
if context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
],
|
||||
)
|
||||
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
if 0 != seconds && !is_sending_locations_before {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::msg_location_enabled(context).await);
|
||||
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if 0 != seconds && !is_sending_locations_before {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(stock_str::msg_location_enabled(context).await);
|
||||
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
|
||||
chat::send_msg(context, chat_id, &mut msg)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, stock_str, now).await;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
job::Action::MaybeSendLocationsEnded,
|
||||
chat_id.to_u32(),
|
||||
Params::new(),
|
||||
seconds + 1,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
.unwrap_or_default();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str = stock_str::msg_location_disabled(context).await;
|
||||
chat::add_info_msg(context, chat_id, stock_str, now).await;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
schedule_maybe_send_locations(context, false).await;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(
|
||||
job::Action::MaybeSendLocationsEnded,
|
||||
chat_id.to_u32(),
|
||||
Params::new(),
|
||||
seconds + 1,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) {
|
||||
@@ -255,25 +256,31 @@ async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool)
|
||||
///
|
||||
/// If `chat_id` is `Some` only that chat is checked, otherwise returns `true` if any chat
|
||||
/// is sending locations.
|
||||
pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<ChatId>) -> bool {
|
||||
match chat_id {
|
||||
Some(chat_id) => context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
paramsv![chat_id, time()],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
None => context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
pub async fn is_sending_locations_to_chat(
|
||||
context: &Context,
|
||||
chat_id: Option<ChatId>,
|
||||
) -> Result<bool> {
|
||||
let exists = match chat_id {
|
||||
Some(chat_id) => {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
paramsv![chat_id, time()],
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
@@ -288,7 +295,11 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
|chats| {
|
||||
chats
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -325,7 +336,7 @@ pub async fn get_range(
|
||||
contact_id: Option<u32>,
|
||||
timestamp_from: i64,
|
||||
mut timestamp_to: i64,
|
||||
) -> Result<Vec<Location>, Error> {
|
||||
) -> Result<Vec<Location>> {
|
||||
if timestamp_to == 0 {
|
||||
timestamp_to = time() + 10;
|
||||
}
|
||||
@@ -400,7 +411,7 @@ fn is_marker(txt: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Deletes all locations from the database.
|
||||
pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM locations;", paramsv![])
|
||||
@@ -409,7 +420,7 @@ pub async fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
let self_addr = context
|
||||
@@ -517,7 +528,7 @@ pub async fn set_kml_sent_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -528,11 +539,7 @@ pub async fn set_kml_sent_timestamp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_msg_location_id(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
location_id: u32,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -550,7 +557,7 @@ pub async fn save(
|
||||
contact_id: u32,
|
||||
locations: &[Location],
|
||||
independent: bool,
|
||||
) -> Result<u32, Error> {
|
||||
) -> Result<u32> {
|
||||
ensure!(!chat_id.is_special(), "Invalid chat id");
|
||||
|
||||
let mut newest_timestamp = 0;
|
||||
@@ -630,7 +637,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
|
||||
},
|
||||
|rows| {
|
||||
rows.filter_map(|v| v.transpose())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
|
||||
15
src/log.rs
15
src/log.rs
@@ -13,7 +13,7 @@ macro_rules! info {
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
emit_event!($ctx, $crate::EventType::Info(full));
|
||||
$ctx.emit_event($crate::EventType::Info(full));
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ macro_rules! warn {
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
emit_event!($ctx, $crate::EventType::Warning(full));
|
||||
$ctx.emit_event($crate::EventType::Warning(full));
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -39,17 +39,10 @@ macro_rules! error {
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
emit_event!($ctx, $crate::EventType::Error(formatted));
|
||||
$ctx.emit_event($crate::EventType::Error(formatted));
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! emit_event {
|
||||
($ctx:expr, $event:expr) => {
|
||||
$ctx.emit_event($event);
|
||||
};
|
||||
}
|
||||
|
||||
pub trait LogExt<T, E>
|
||||
where
|
||||
Self: std::marker::Sized,
|
||||
@@ -136,7 +129,7 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
||||
);
|
||||
// We can't use the warn!() macro here as the file!() and line!() macros
|
||||
// don't work with #[track_caller]
|
||||
emit_event!(context, crate::EventType::Warning(full));
|
||||
context.emit_event(crate::EventType::Warning(full));
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
39
src/lot.rs
39
src/lot.rs
@@ -3,6 +3,8 @@
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
|
||||
use crate::key::Fingerprint;
|
||||
use crate::message::MessageState;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
@@ -139,3 +141,40 @@ impl Default for LotState {
|
||||
LotState::Undefined
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageState> for LotState {
|
||||
fn from(s: MessageState) -> Self {
|
||||
use MessageState::*;
|
||||
match s {
|
||||
Undefined => LotState::Undefined,
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
OutDelivered => LotState::MsgOutDelivered,
|
||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Summary> for Lot {
|
||||
fn from(summary: Summary) -> Self {
|
||||
let (text1, text1_meaning) = match summary.prefix {
|
||||
None => (None, Meaning::None),
|
||||
Some(SummaryPrefix::Draft(text)) => (Some(text), Meaning::Text1Draft),
|
||||
Some(SummaryPrefix::Username(username)) => (Some(username), Meaning::Text1Username),
|
||||
Some(SummaryPrefix::Me(text)) => (Some(text), Meaning::Text1Self),
|
||||
};
|
||||
Self {
|
||||
text1_meaning,
|
||||
text1,
|
||||
text2: Some(summary.text),
|
||||
timestamp: summary.timestamp,
|
||||
state: summary.state.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
418
src/message.rs
418
src/message.rs
@@ -3,10 +3,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{ensure, format_err, Result};
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use async_std::path::{Path, PathBuf};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use itertools::Itertools;
|
||||
use rusqlite::types::ValueRef;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -22,19 +21,16 @@ use crate::dc_tools::{
|
||||
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
|
||||
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
|
||||
};
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
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::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::stock_str;
|
||||
|
||||
// In practice, the user additionally cuts the string themselves
|
||||
// pixel-accurate.
|
||||
const SUMMARY_CHARACTERS: usize = 160;
|
||||
use crate::summary::{get_summarytext_by_raw, Summary};
|
||||
|
||||
/// Message ID, including reserved IDs.
|
||||
///
|
||||
@@ -301,6 +297,7 @@ pub struct Message {
|
||||
pub(crate) chat_id: ChatId,
|
||||
pub(crate) viewtype: Viewtype,
|
||||
pub(crate) state: MessageState,
|
||||
pub(crate) download_state: DownloadState,
|
||||
pub(crate) hidden: bool,
|
||||
pub(crate) timestamp_sort: i64,
|
||||
pub(crate) timestamp_sent: i64,
|
||||
@@ -355,6 +352,7 @@ impl Message {
|
||||
" m.ephemeral_timestamp AS ephemeral_timestamp,",
|
||||
" m.type AS type,",
|
||||
" m.state AS state,",
|
||||
" m.download_state AS download_state,",
|
||||
" m.error AS error,",
|
||||
" m.msgrmsg AS msgrmsg,",
|
||||
" m.mime_modified AS mime_modified,",
|
||||
@@ -406,6 +404,7 @@ impl Message {
|
||||
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
|
||||
viewtype: row.get("type")?,
|
||||
state: row.get("state")?,
|
||||
download_state: row.get("download_state")?,
|
||||
error: Some(row.get::<_, String>("error")?)
|
||||
.filter(|error| !error.is_empty()),
|
||||
is_dc_message: row.get("msgrmsg")?,
|
||||
@@ -592,23 +591,21 @@ impl Message {
|
||||
self.ephemeral_timestamp
|
||||
}
|
||||
|
||||
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot {
|
||||
let mut ret = Lot::new();
|
||||
|
||||
/// Returns message summary for display in the search results.
|
||||
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
|
||||
let chat_loaded: Chat;
|
||||
let chat = if let Some(chat) = chat {
|
||||
chat
|
||||
} else if let Ok(chat) = Chat::load_from_db(context, self.chat_id).await {
|
||||
} else {
|
||||
let chat = Chat::load_from_db(context, self.chat_id).await?;
|
||||
chat_loaded = chat;
|
||||
&chat_loaded
|
||||
} else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let contact = if self.from_id != DC_CONTACT_ID_SELF {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
Contact::get_by_id(context, self.from_id).await.ok()
|
||||
Some(Contact::get_by_id(context, self.from_id).await?)
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => None,
|
||||
}
|
||||
@@ -616,9 +613,7 @@ impl Message {
|
||||
None
|
||||
};
|
||||
|
||||
ret.fill(self, chat, contact.as_ref(), context).await;
|
||||
|
||||
ret
|
||||
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
|
||||
}
|
||||
|
||||
pub async fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String {
|
||||
@@ -1029,24 +1024,6 @@ impl std::fmt::Display for MessageState {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageState> for LotState {
|
||||
fn from(s: MessageState) -> Self {
|
||||
use MessageState::*;
|
||||
match s {
|
||||
Undefined => LotState::Undefined,
|
||||
InFresh => LotState::MsgInFresh,
|
||||
InNoticed => LotState::MsgInNoticed,
|
||||
InSeen => LotState::MsgInSeen,
|
||||
OutPreparing => LotState::MsgOutPreparing,
|
||||
OutDraft => LotState::MsgOutDraft,
|
||||
OutPending => LotState::MsgOutPending,
|
||||
OutFailed => LotState::MsgOutFailed,
|
||||
OutDelivered => LotState::MsgOutDelivered,
|
||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageState {
|
||||
pub fn can_fail(self) -> bool {
|
||||
use MessageState::*;
|
||||
@@ -1064,68 +1041,6 @@ impl MessageState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
/* library-internal */
|
||||
/* in practice, the user additionally cuts the string himself pixel-accurate */
|
||||
pub async fn fill(
|
||||
&mut self,
|
||||
msg: &mut Message,
|
||||
chat: &Chat,
|
||||
contact: Option<&Contact>,
|
||||
context: &Context,
|
||||
) {
|
||||
if msg.state == MessageState::OutDraft {
|
||||
self.text1 = Some(stock_str::draft(context).await);
|
||||
self.text1_meaning = Meaning::Text1Draft;
|
||||
} else if msg.from_id == DC_CONTACT_ID_SELF {
|
||||
if msg.is_info() || chat.is_self_talk() {
|
||||
self.text1 = None;
|
||||
self.text1_meaning = Meaning::None;
|
||||
} else {
|
||||
self.text1 = Some(stock_str::self_msg(context).await);
|
||||
self.text1_meaning = Meaning::Text1Self;
|
||||
}
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
if msg.is_info() || contact.is_none() {
|
||||
self.text1 = None;
|
||||
self.text1_meaning = Meaning::None;
|
||||
} else {
|
||||
self.text1 = msg
|
||||
.get_override_sender_name()
|
||||
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)));
|
||||
self.text1_meaning = Meaning::Text1Username;
|
||||
}
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => {
|
||||
self.text1 = None;
|
||||
self.text1_meaning = Meaning::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text2 = get_summarytext_by_raw(
|
||||
msg.viewtype,
|
||||
msg.text.as_ref(),
|
||||
msg.is_forwarded(),
|
||||
&msg.param,
|
||||
SUMMARY_CHARACTERS,
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
|
||||
if text2.is_empty() && msg.quoted_text().is_some() {
|
||||
text2 = stock_str::reply_noun(context).await
|
||||
}
|
||||
|
||||
self.text2 = Some(text2);
|
||||
|
||||
self.timestamp = msg.get_timestamp();
|
||||
self.state = msg.state.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let rawtxt: Option<String> = context
|
||||
@@ -1366,16 +1281,16 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
for msg_id in msg_ids.iter() {
|
||||
if let Ok(msg) = Message::load_from_db(context, *msg_id).await {
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await;
|
||||
}
|
||||
}
|
||||
if let Err(err) = msg_id.trash(context).await {
|
||||
error!(context, "Unable to trash message {}: {}", msg_id, err);
|
||||
let msg = Message::load_from_db(context, *msg_id).await?;
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await;
|
||||
}
|
||||
msg_id
|
||||
.trash(context)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {}", msg_id))?;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0),
|
||||
@@ -1388,13 +1303,14 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
job::kill_action(context, Action::Housekeeping).await;
|
||||
job::kill_action(context, Action::Housekeeping).await?;
|
||||
job::add(
|
||||
context,
|
||||
job::Job::new(Action::Housekeeping, 0, Params::new(), 10),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
|
||||
@@ -1490,88 +1406,6 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Returns a summary text.
|
||||
pub async fn get_summarytext_by_raw(
|
||||
viewtype: Viewtype,
|
||||
text: Option<impl AsRef<str>>,
|
||||
was_forwarded: bool,
|
||||
param: &Params,
|
||||
approx_characters: usize,
|
||||
context: &Context,
|
||||
) -> String {
|
||||
let mut append_text = true;
|
||||
let prefix = match viewtype {
|
||||
Viewtype::Image => stock_str::image(context).await,
|
||||
Viewtype::Gif => stock_str::gif(context).await,
|
||||
Viewtype::Sticker => stock_str::sticker(context).await,
|
||||
Viewtype::Video => stock_str::video(context).await,
|
||||
Viewtype::Voice => stock_str::voice_message(context).await,
|
||||
Viewtype::Audio | Viewtype::File => {
|
||||
if param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
append_text = false;
|
||||
stock_str::ac_setup_msg_subject(context).await
|
||||
} else {
|
||||
let file_name: String = param
|
||||
.get_path(Param::File, context)
|
||||
.unwrap_or(None)
|
||||
.and_then(|path| {
|
||||
path.file_name()
|
||||
.map(|fname| fname.to_string_lossy().into_owned())
|
||||
})
|
||||
.unwrap_or_else(|| String::from("ErrFileName"));
|
||||
let label = if viewtype == Viewtype::Audio {
|
||||
stock_str::audio(context).await
|
||||
} else {
|
||||
stock_str::file(context).await
|
||||
};
|
||||
format!("{} – {}", label, file_name)
|
||||
}
|
||||
}
|
||||
Viewtype::VideochatInvitation => {
|
||||
append_text = false;
|
||||
stock_str::videochat_invitation(context).await
|
||||
}
|
||||
_ => {
|
||||
if param.get_cmd() != SystemMessage::LocationOnly {
|
||||
"".to_string()
|
||||
} else {
|
||||
append_text = false;
|
||||
stock_str::location(context).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !append_text {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
let summary_content = if let Some(text) = text {
|
||||
if text.as_ref().is_empty() {
|
||||
prefix
|
||||
} else if prefix.is_empty() {
|
||||
dc_truncate(text.as_ref(), approx_characters).to_string()
|
||||
} else {
|
||||
let tmp = format!("{} – {}", prefix, text.as_ref());
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
}
|
||||
} else {
|
||||
prefix
|
||||
};
|
||||
|
||||
let summary = if was_forwarded {
|
||||
let tmp = format!(
|
||||
"{}: {}",
|
||||
stock_str::forwarded(context).await,
|
||||
summary_content
|
||||
);
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
} else {
|
||||
summary_content
|
||||
};
|
||||
|
||||
summary.split_whitespace().join(" ")
|
||||
}
|
||||
|
||||
// as we do not cut inside words, this results in about 32-42 characters.
|
||||
// Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise.
|
||||
// It should also be very clear, the subject is _not_ the whole message.
|
||||
@@ -1635,6 +1469,17 @@ pub async fn handle_mdn(
|
||||
rfc724_mid: &str,
|
||||
timestamp_sent: i64,
|
||||
) -> Result<Option<(ChatId, MsgId)>> {
|
||||
if from_id == DC_CONTACT_ID_SELF {
|
||||
warn!(
|
||||
context,
|
||||
"ignoring MDN sent to self, this is a bug on the sender device"
|
||||
);
|
||||
|
||||
// This is not an error on our side,
|
||||
// we successfully ignored an invalid MDN and return `Ok`.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
@@ -2238,203 +2083,6 @@ mod tests {
|
||||
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summarytext_by_raw() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let some_text = Some(" bla \t\n\tbla\n\t".to_string());
|
||||
let empty_text = Some("".to_string());
|
||||
let no_text: Option<String> = None;
|
||||
|
||||
let mut some_file = Params::new();
|
||||
some_file.set(Param::File, "foo.bar");
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Text,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&Params::new(),
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"bla bla" // for simple text, the type is not added to the summary
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Image,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Image" // file names are not added for images
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Video,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Video" // file names are not added for videos
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), false, &some_file, 50, ctx,)
|
||||
.await,
|
||||
"GIF" // file names are not added for GIFs
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Sticker,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
"Sticker" // file names are not added for stickers
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
empty_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
"Voice message" // file names are not added for voice messages, empty text is skipped
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Voice message" // file names are not added for voice messages
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
empty_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::File,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
|
||||
);
|
||||
|
||||
// Forwarded
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Text,
|
||||
some_text.as_ref(),
|
||||
true,
|
||||
&Params::new(),
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Forwarded: bla bla" // for simple text, the type is not added to the summary
|
||||
);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::File,
|
||||
some_text.as_ref(),
|
||||
true,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
|
||||
);
|
||||
|
||||
let mut asm_file = Params::new();
|
||||
asm_file.set(Param::File, "foo.bar");
|
||||
asm_file.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), false, &asm_file, 50, ctx)
|
||||
.await,
|
||||
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_webrtc_instance() {
|
||||
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
|
||||
|
||||
@@ -388,10 +388,6 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
let subject = match self.loaded {
|
||||
Loaded::Message { ref chat } => {
|
||||
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
return Ok(stock_str::ac_setup_msg_subject(context).await);
|
||||
}
|
||||
|
||||
if !self.msg.subject.is_empty() {
|
||||
return Ok(self.msg.subject.clone());
|
||||
}
|
||||
@@ -1084,7 +1080,7 @@ impl<'a> MimeFactory<'a> {
|
||||
parts.push(msg_kml_part);
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await {
|
||||
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
|
||||
match self.get_location_kml_part(context).await {
|
||||
Ok(part) => parts.push(part),
|
||||
Err(err) => {
|
||||
|
||||
@@ -136,6 +136,18 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
|
||||
impl MimeMessage {
|
||||
pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
|
||||
MimeMessage::from_bytes_with_partial(context, body, None).await
|
||||
}
|
||||
|
||||
/// Parse a mime message.
|
||||
///
|
||||
/// If `partial` is set, it contains the full message size in bytes
|
||||
/// and `body` contains the header only.
|
||||
pub async fn from_bytes_with_partial(
|
||||
context: &Context,
|
||||
body: &[u8],
|
||||
partial: Option<u32>,
|
||||
) -> Result<Self> {
|
||||
let mail = mailparse::parse_mail(body)?;
|
||||
|
||||
let message_time = mail
|
||||
@@ -274,7 +286,18 @@ impl MimeMessage {
|
||||
is_mime_modified: false,
|
||||
decoded_data: Vec::new(),
|
||||
};
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
|
||||
match partial {
|
||||
Some(org_bytes) => {
|
||||
parser
|
||||
.create_stub_from_partial_download(context, org_bytes)
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
parser.parse_mime_recursive(context, &mail, false).await?;
|
||||
}
|
||||
};
|
||||
|
||||
parser.maybe_remove_bad_parts();
|
||||
parser.maybe_remove_inline_mailinglist_footer();
|
||||
parser.heuristically_parse_ndn(context).await;
|
||||
@@ -1141,11 +1164,11 @@ impl MimeMessage {
|
||||
report: &mailparse::ParsedMail<'_>,
|
||||
) -> Result<Option<FailureReport>> {
|
||||
// parse as mailheaders
|
||||
if let Some(original_msg) = report
|
||||
.subparts
|
||||
.iter()
|
||||
.find(|p| p.ctype.mimetype.contains("rfc822") || p.ctype.mimetype == "message/global")
|
||||
{
|
||||
if let Some(original_msg) = report.subparts.iter().find(|p| {
|
||||
p.ctype.mimetype.contains("rfc822")
|
||||
|| p.ctype.mimetype == "message/global"
|
||||
|| p.ctype.mimetype == "message/global-headers"
|
||||
}) {
|
||||
let report_body = original_msg.get_body_raw()?;
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
@@ -1443,9 +1466,9 @@ pub struct Part {
|
||||
pub msg_raw: Option<String>,
|
||||
pub bytes: usize,
|
||||
pub param: Params,
|
||||
org_filename: Option<String>,
|
||||
pub(crate) org_filename: Option<String>,
|
||||
pub error: Option<String>,
|
||||
dehtml_failed: bool,
|
||||
pub(crate) dehtml_failed: bool,
|
||||
|
||||
/// the part is a child or a descendant of multipart/related.
|
||||
/// typically, these are images that are referenced from text/html part
|
||||
@@ -1453,7 +1476,7 @@ pub struct Part {
|
||||
///
|
||||
/// note that multipart/related may contain further multipart nestings
|
||||
/// and all of them needs to be marked with `is_related`.
|
||||
is_related: bool,
|
||||
pub(crate) is_related: bool,
|
||||
}
|
||||
|
||||
/// return mimetype and viewtype for a parsed mail
|
||||
@@ -2997,4 +3020,74 @@ Message.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_ignore_read_receipt_to_self() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Alice receives BCC-self copy of a message sent to Bob.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.com\n\
|
||||
To: bob@example.net\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: first@example.com\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Disposition-Notification-To: alice@example.com\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello\n"
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
|
||||
// Due to a bug in the old version running on the other device, Alice receives a read
|
||||
// receipt from self.
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
From: alice@example.com\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: message opened\n\
|
||||
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: second@example.com\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
Read receipts do not guarantee sth. was read.\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP\n\
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Original-Recipient: rfc822;bob@example.com\n\
|
||||
Final-Recipient: rfc822;bob@example.com\n\
|
||||
Original-Message-ID: <first@example.com>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n\
|
||||
\n\
|
||||
--SNIPP--"
|
||||
.as_bytes(),
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check that the state has not changed to `MessageState::OutMdnRcvd`.
|
||||
let msg = Message::load_from_db(&alice, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
32
src/param.rs
32
src/param.rs
@@ -139,6 +139,27 @@ pub enum Param {
|
||||
|
||||
/// For MDN-sending job
|
||||
MsgId = b'I',
|
||||
|
||||
/// For Contacts: timestamp of status (aka signature or footer) update.
|
||||
StatusTimestamp = b'j',
|
||||
|
||||
/// For Contacts and Chats: timestamp of avatar update.
|
||||
AvatarTimestamp = b'J',
|
||||
|
||||
/// For Chats: timestamp of status/signature/footer update.
|
||||
EphemeralSettingsTimestamp = b'B',
|
||||
|
||||
/// For Chats: timestamp of subject update.
|
||||
SubjectTimestamp = b'C',
|
||||
|
||||
/// For Chats: timestamp of group name update.
|
||||
GroupNameTimestamp = b'g',
|
||||
|
||||
/// For Chats: timestamp of group name update.
|
||||
MemberListTimestamp = b'k',
|
||||
|
||||
/// For Chats: timestamp of protection settings update.
|
||||
ProtectionSettingsTimestamp = b'L',
|
||||
}
|
||||
|
||||
/// An object for handling key=value parameter lists.
|
||||
@@ -245,6 +266,11 @@ impl Params {
|
||||
self.get(key).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Get the given parameter and parse as `i64`.
|
||||
pub fn get_i64(&self, key: Param) -> Option<i64> {
|
||||
self.get(key).and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Get the given parameter and parse as `bool`.
|
||||
pub fn get_bool(&self, key: Param) -> Option<bool> {
|
||||
self.get_int(key).map(|v| v != 0)
|
||||
@@ -346,6 +372,12 @@ impl Params {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given paramter to the passed in `i64`.
|
||||
pub fn set_i64(&mut self, key: Param, value: i64) -> &mut Self {
|
||||
self.set(key, value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given parameter to the passed in `f64` .
|
||||
pub fn set_float(&mut self, key: Param, value: f64) -> &mut Self {
|
||||
self.set(key, format!("{}", value));
|
||||
|
||||
@@ -278,7 +278,7 @@ impl Peerstate {
|
||||
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
|
||||
|
||||
chat::add_info_msg(context, chat_id, msg, timestamp).await;
|
||||
emit_event!(context, EventType::ChatModified(chat_id));
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
} else {
|
||||
bail!("contact with peerstate.addr {:?} not found", &self.addr);
|
||||
}
|
||||
|
||||
@@ -98,13 +98,14 @@ fn get_highest_usage<'t>(
|
||||
|
||||
impl Context {
|
||||
// Adds a job to update `quota.recent`
|
||||
pub(crate) async fn schedule_quota_update(&self) {
|
||||
job::kill_action(self, Action::UpdateRecentQuota).await;
|
||||
pub(crate) async fn schedule_quota_update(&self) -> Result<()> {
|
||||
job::kill_action(self, Action::UpdateRecentQuota).await?;
|
||||
job::add(
|
||||
self,
|
||||
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates `quota.recent`, sets `quota.modified` to the current time
|
||||
|
||||
@@ -82,7 +82,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
let mut jobs_loaded = 0;
|
||||
let mut info = InterruptInfo::default();
|
||||
loop {
|
||||
match job::load_next(&ctx, Thread::Imap, &info).await {
|
||||
match job::load_next(&ctx, Thread::Imap, &info)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
Some(job) if jobs_loaded <= 20 => {
|
||||
jobs_loaded += 1;
|
||||
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
|
||||
@@ -289,7 +293,11 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
|
||||
let mut interrupt_info = Default::default();
|
||||
loop {
|
||||
match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
|
||||
match job::load_next(&ctx, Thread::Smtp, &interrupt_info)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
Some(job) => {
|
||||
info!(ctx, "executing smtp job");
|
||||
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;
|
||||
|
||||
@@ -504,11 +504,11 @@ impl Context {
|
||||
}
|
||||
|
||||
if quota.modified + QUOTA_MAX_AGE_SECONDS < time() {
|
||||
self.schedule_quota_update().await;
|
||||
self.schedule_quota_update().await?;
|
||||
}
|
||||
} else {
|
||||
ret += "<li>One moment...</li>";
|
||||
self.schedule_quota_update().await;
|
||||
self.schedule_quota_update().await?;
|
||||
}
|
||||
ret += "</ul>";
|
||||
|
||||
|
||||
@@ -635,10 +635,10 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
|
||||
info!(context, "Auth verified.",);
|
||||
secure_connection_established(context, contact_chat_id).await?;
|
||||
emit_event!(context, EventType::ContactsChanged(Some(contact_id)));
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
// the vg-member-added message is special:
|
||||
@@ -861,7 +861,7 @@ async fn secure_connection_established(
|
||||
};
|
||||
let msg = stock_str::contact_verified(context, addr).await;
|
||||
chat::add_info_msg(context, contact_chat_id, msg, time()).await;
|
||||
emit_event!(context, EventType::ChatModified(contact_chat_id));
|
||||
context.emit_event(EventType::ChatModified(contact_chat_id));
|
||||
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -357,8 +357,8 @@ impl BobState {
|
||||
}
|
||||
mark_peer_as_verified(context, self.invite.fingerprint()).await?;
|
||||
Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined)
|
||||
.await;
|
||||
emit_event!(context, EventType::ContactsChanged(None));
|
||||
.await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
if let QrInvite::Group { .. } = self.invite {
|
||||
let member_added = mime_message
|
||||
|
||||
@@ -109,7 +109,8 @@ impl Smtp {
|
||||
&lp.socks5_config,
|
||||
&lp.addr,
|
||||
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
|
||||
lp.provider.map_or(false, |provider| provider.strict_tls),
|
||||
lp.provider
|
||||
.map_or(lp.socks5_config.is_some(), |provider| provider.strict_tls),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -598,7 +598,7 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
context.schedule_quota_update().await;
|
||||
context.schedule_quota_update().await?;
|
||||
|
||||
if let Err(e) = context
|
||||
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
|
||||
|
||||
@@ -477,6 +477,16 @@ paramsv![]
|
||||
sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 79 {
|
||||
info!(context, "[migration] v79");
|
||||
sql.execute_migration(
|
||||
r#"
|
||||
ALTER TABLE msgs ADD COLUMN download_state INTEGER DEFAULT 0;
|
||||
"#,
|
||||
79,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
|
||||
@@ -13,8 +13,10 @@ use crate::config::Config;
|
||||
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::dc_timestamp_to_str;
|
||||
use crate::message::Message;
|
||||
use crate::param::Param;
|
||||
use humansize::{file_size_opts, FileSize};
|
||||
|
||||
/// Stock strings
|
||||
///
|
||||
@@ -267,6 +269,12 @@ pub enum StockMessage {
|
||||
You can check your current storage usage anytime at \"Settings / Connectivity\"."
|
||||
))]
|
||||
QuotaExceedingMsgBody = 98,
|
||||
|
||||
#[strum(props(fallback = "%1$s message"))]
|
||||
PartialDownloadMsgBody = 99,
|
||||
|
||||
#[strum(props(fallback = "Download maximum available until %1$s"))]
|
||||
DownloadAvailability = 100,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -857,6 +865,23 @@ pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> St
|
||||
.replace("%%", "%")
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
|
||||
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
|
||||
let size = org_bytes
|
||||
.file_size(file_size_opts::BINARY)
|
||||
.unwrap_or_default();
|
||||
translated(context, StockMessage::PartialDownloadMsgBody)
|
||||
.await
|
||||
.replace1(size)
|
||||
}
|
||||
|
||||
/// Stock string: `Download maximum available until %1$s`.
|
||||
pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
|
||||
translated(context, StockMessage::DownloadAvailability)
|
||||
.await
|
||||
.replace1(dc_timestamp_to_str(timestamp))
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Set the stock string for the [StockMessage].
|
||||
///
|
||||
@@ -1050,6 +1075,14 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_partial_download_msg_body() -> anyhow::Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let str = partial_download_msg_body(&t, 1024 * 1024).await;
|
||||
assert_eq!(str, "1 MiB message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_update_device_chats() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
394
src/summary.rs
Normal file
394
src/summary.rs
Normal file
@@ -0,0 +1,394 @@
|
||||
//! # Message summary for chatlist.
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::constants::{Chattype, Viewtype, DC_CONTACT_ID_SELF};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::dc_truncate;
|
||||
use crate::message::{Message, MessageState};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::stock_str;
|
||||
use itertools::Itertools;
|
||||
use std::fmt;
|
||||
|
||||
// In practice, the user additionally cuts the string themselves
|
||||
// pixel-accurate.
|
||||
const SUMMARY_CHARACTERS: usize = 160;
|
||||
|
||||
/// Prefix displayed before message and separated by ":" in the chatlist.
|
||||
#[derive(Debug)]
|
||||
pub enum SummaryPrefix {
|
||||
/// Username.
|
||||
Username(String),
|
||||
|
||||
/// Stock string saying "Draft".
|
||||
Draft(String),
|
||||
|
||||
/// Stock string saying "Me".
|
||||
Me(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for SummaryPrefix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
SummaryPrefix::Username(username) => write!(f, "{}", username),
|
||||
SummaryPrefix::Draft(text) => write!(f, "{}", text),
|
||||
SummaryPrefix::Me(text) => write!(f, "{}", text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message summary.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Summary {
|
||||
/// Part displayed before ":", such as an username or a string "Draft".
|
||||
pub prefix: Option<SummaryPrefix>,
|
||||
|
||||
/// Summary text, always present.
|
||||
pub text: String,
|
||||
|
||||
/// Message timestamp.
|
||||
pub timestamp: i64,
|
||||
|
||||
/// Message state.
|
||||
pub state: MessageState,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
pub async fn new(
|
||||
context: &Context,
|
||||
msg: &Message,
|
||||
chat: &Chat,
|
||||
contact: Option<&Contact>,
|
||||
) -> Self {
|
||||
let prefix = if msg.state == MessageState::OutDraft {
|
||||
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
|
||||
} else if msg.from_id == DC_CONTACT_ID_SELF {
|
||||
if msg.is_info() || chat.is_self_talk() {
|
||||
None
|
||||
} else {
|
||||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||||
}
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
if msg.is_info() || contact.is_none() {
|
||||
None
|
||||
} else {
|
||||
msg.get_override_sender_name()
|
||||
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
|
||||
.map(SummaryPrefix::Username)
|
||||
}
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => None,
|
||||
}
|
||||
};
|
||||
|
||||
let mut text = get_summarytext_by_raw(
|
||||
msg.viewtype,
|
||||
msg.text.as_ref(),
|
||||
msg.is_forwarded(),
|
||||
&msg.param,
|
||||
SUMMARY_CHARACTERS,
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
|
||||
if text.is_empty() && msg.quoted_text().is_some() {
|
||||
text = stock_str::reply_noun(context).await
|
||||
}
|
||||
|
||||
Self {
|
||||
prefix,
|
||||
text,
|
||||
timestamp: msg.get_timestamp(),
|
||||
state: msg.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a summary text.
|
||||
pub async fn get_summarytext_by_raw(
|
||||
viewtype: Viewtype,
|
||||
text: Option<impl AsRef<str>>,
|
||||
was_forwarded: bool,
|
||||
param: &Params,
|
||||
approx_characters: usize,
|
||||
context: &Context,
|
||||
) -> String {
|
||||
let mut append_text = true;
|
||||
let prefix = match viewtype {
|
||||
Viewtype::Image => stock_str::image(context).await,
|
||||
Viewtype::Gif => stock_str::gif(context).await,
|
||||
Viewtype::Sticker => stock_str::sticker(context).await,
|
||||
Viewtype::Video => stock_str::video(context).await,
|
||||
Viewtype::Voice => stock_str::voice_message(context).await,
|
||||
Viewtype::Audio | Viewtype::File => {
|
||||
if param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
append_text = false;
|
||||
stock_str::ac_setup_msg_subject(context).await
|
||||
} else {
|
||||
let file_name: String = param
|
||||
.get_path(Param::File, context)
|
||||
.unwrap_or(None)
|
||||
.and_then(|path| {
|
||||
path.file_name()
|
||||
.map(|fname| fname.to_string_lossy().into_owned())
|
||||
})
|
||||
.unwrap_or_else(|| String::from("ErrFileName"));
|
||||
let label = if viewtype == Viewtype::Audio {
|
||||
stock_str::audio(context).await
|
||||
} else {
|
||||
stock_str::file(context).await
|
||||
};
|
||||
format!("{} – {}", label, file_name)
|
||||
}
|
||||
}
|
||||
Viewtype::VideochatInvitation => {
|
||||
append_text = false;
|
||||
stock_str::videochat_invitation(context).await
|
||||
}
|
||||
_ => {
|
||||
if param.get_cmd() != SystemMessage::LocationOnly {
|
||||
"".to_string()
|
||||
} else {
|
||||
append_text = false;
|
||||
stock_str::location(context).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !append_text {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
let summary_content = if let Some(text) = text {
|
||||
if text.as_ref().is_empty() {
|
||||
prefix
|
||||
} else if prefix.is_empty() {
|
||||
dc_truncate(text.as_ref(), approx_characters).to_string()
|
||||
} else {
|
||||
let tmp = format!("{} – {}", prefix, text.as_ref());
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
}
|
||||
} else {
|
||||
prefix
|
||||
};
|
||||
|
||||
let summary = if was_forwarded {
|
||||
let tmp = format!(
|
||||
"{}: {}",
|
||||
stock_str::forwarded(context).await,
|
||||
summary_content
|
||||
);
|
||||
dc_truncate(&tmp, approx_characters).to_string()
|
||||
} else {
|
||||
summary_content
|
||||
};
|
||||
|
||||
summary.split_whitespace().join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils as test;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_summarytext_by_raw() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let some_text = Some(" bla \t\n\tbla\n\t".to_string());
|
||||
let empty_text = Some("".to_string());
|
||||
let no_text: Option<String> = None;
|
||||
|
||||
let mut some_file = Params::new();
|
||||
some_file.set(Param::File, "foo.bar");
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Text,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&Params::new(),
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"bla bla" // for simple text, the type is not added to the summary
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Image,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Image" // file names are not added for images
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Video,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Video" // file names are not added for videos
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), false, &some_file, 50, ctx,)
|
||||
.await,
|
||||
"GIF" // file names are not added for GIFs
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Sticker,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
"Sticker" // file names are not added for stickers
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
empty_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
"Voice message" // file names are not added for voice messages, empty text is skipped
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Voice message" // file names are not added for voice messages
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Voice,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
no_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
empty_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx,
|
||||
)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Audio,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::File,
|
||||
some_text.as_ref(),
|
||||
false,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
|
||||
);
|
||||
|
||||
// Forwarded
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::Text,
|
||||
some_text.as_ref(),
|
||||
true,
|
||||
&Params::new(),
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Forwarded: bla bla" // for simple text, the type is not added to the summary
|
||||
);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(
|
||||
Viewtype::File,
|
||||
some_text.as_ref(),
|
||||
true,
|
||||
&some_file,
|
||||
50,
|
||||
ctx
|
||||
)
|
||||
.await,
|
||||
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
|
||||
);
|
||||
|
||||
let mut asm_file = Params::new();
|
||||
asm_file.set(Param::File, "foo.bar");
|
||||
asm_file.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
assert_eq!(
|
||||
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), false, &asm_file, 50, ctx)
|
||||
.await,
|
||||
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
|
||||
);
|
||||
}
|
||||
}
|
||||
197
src/update_helper.rs
Normal file
197
src/update_helper.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
//! # Functions to update timestamps.
|
||||
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::param::{Param, Params};
|
||||
use anyhow::Result;
|
||||
|
||||
impl Context {
|
||||
/// Updates a contact's timestamp, if reasonable.
|
||||
/// Returns true if the caller shall update the settings belonging to the scope.
|
||||
/// (if we have a ContactId type at some point, the function should go there)
|
||||
pub(crate) async fn update_contacts_timestamp(
|
||||
&self,
|
||||
contact_id: u32,
|
||||
scope: Param,
|
||||
new_timestamp: i64,
|
||||
) -> Result<bool> {
|
||||
let mut contact = Contact::load_from_db(self, contact_id).await?;
|
||||
if contact.param.update_timestamp(scope, new_timestamp)? {
|
||||
contact.update_param(self).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatId {
|
||||
/// Updates a chat id's timestamp on disk, if reasonable.
|
||||
/// Returns true if the caller shall update the settings belonging to the scope.
|
||||
pub(crate) async fn update_timestamp(
|
||||
&self,
|
||||
context: &Context,
|
||||
scope: Param,
|
||||
new_timestamp: i64,
|
||||
) -> Result<bool> {
|
||||
let mut chat = Chat::load_from_db(context, *self).await?;
|
||||
if chat.param.update_timestamp(scope, new_timestamp)? {
|
||||
chat.update_param(context).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Params {
|
||||
/// Updates a param's timestamp in memory, if reasonable.
|
||||
/// Returns true if the caller shall update the settings belonging to the scope.
|
||||
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> Result<bool> {
|
||||
let old_timestamp = self.get_i64(scope).unwrap_or_default();
|
||||
if new_timestamp >= old_timestamp {
|
||||
self.set_i64(scope, new_timestamp);
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::time;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_params_update_timestamp() -> Result<()> {
|
||||
let mut params = Params::new();
|
||||
let ts = time();
|
||||
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?);
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts)?); // same timestamp -> update
|
||||
assert!(params.update_timestamp(Param::LastSubject, ts + 10)?);
|
||||
assert!(!params.update_timestamp(Param::LastSubject, ts)?); // `ts` is now too old
|
||||
assert!(!params.update_timestamp(Param::LastSubject, 0)?);
|
||||
assert_eq!(params.get_i64(Param::LastSubject).unwrap(), ts + 10);
|
||||
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // stay unset -> update ...
|
||||
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // ... also on multiple calls
|
||||
assert_eq!(params.get_i64(Param::GroupNameTimestamp).unwrap(), 0);
|
||||
|
||||
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1)?);
|
||||
assert_eq!(params.get_i64(Param::AvatarTimestamp), None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_out_of_order_subject() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: updated subject\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 23:37:57 +0000\n\
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Subject: original subject\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
|
||||
\n\
|
||||
first message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(
|
||||
chat.param.get(Param::LastSubject).unwrap(),
|
||||
"updated subject"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_out_of_order_group_name() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <msg1@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
Chat-Group-Name: initial name\n\
|
||||
Date: Sun, 22 Mar 2021 01:00:00 +0000\n\
|
||||
\n\
|
||||
first message\n",
|
||||
"INBOX",
|
||||
1,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(chat.name, "initial name");
|
||||
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <msg3@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
Chat-Group-Name: another name update\n\
|
||||
Chat-Group-Name-Changed: a name update\n\
|
||||
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
|
||||
\n\
|
||||
third message\n",
|
||||
"INBOX",
|
||||
2,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.com\n\
|
||||
Message-ID: <msg2@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde\n\
|
||||
Chat-Group-Name: a name update\n\
|
||||
Chat-Group-Name-Changed: initial name\n\
|
||||
Date: Sun, 22 Mar 2021 02:00:00 +0000\n\
|
||||
\n\
|
||||
second message\n",
|
||||
"INBOX",
|
||||
3,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert_eq!(chat.name, "another name update");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
60
test-data/message/mailinglist_ttline.eml
Normal file
60
test-data/message/mailinglist_ttline.eml
Normal file
@@ -0,0 +1,60 @@
|
||||
Return-Path: <return@t.ttline.com>
|
||||
X-Original-To: pdetersen@b123.com
|
||||
Delivered-To: m123123f@d123123.kasserver.com
|
||||
X-policyd-weight: NOT_IN_SPAMCOP=-1.5 NOT_IN_IX_MANITU=-1.5 CL_IP_EQ_HELO_IP=-2 (check from: .ttline. - helo: .mail18-212.srv2. - helo-domain: .srv2.) FROM/MX_MATCHES_HELO(DOMAIN)=-2; rate: -7
|
||||
Authentication-Results: d123123.kasserver.com;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=ttline.com header.i=newsletter@ttline.com header.b="SEFAAx0a";
|
||||
dkim=pass (1024-bit key; unprotected) header.d=srv2.de header.i=@srv2.de header.b="UqUBlHLF";
|
||||
dkim-atps=neutral
|
||||
Received: from mail18-212.srv2.de (mail18-212.srv2.de [193.169.181.212])
|
||||
by d123123.kasserver.com (Postfix) with ESMTPS id 4216753C0228
|
||||
for <pdetersen@b123.com>; Mon, 12 Jul 2021 18:00:55 +0200 (CEST)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; s=mailing; d=ttline.com;
|
||||
h=Date:From:Reply-To:To:Message-ID:Subject:MIME-Version:Content-Type:X-ulpe:
|
||||
List-Id:X-CSA-Complaints:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID;
|
||||
i=newsletter@ttline.com;
|
||||
bh=s/Rjfns4gt9JjcDSSsqHZctvWTOtocDJRpEVs80pElM=;
|
||||
b=SEFAAx0aG2fD5fytZ1z0WI2elUpWh5J+ekno+UQE/PDqc4bwz5xEUGRXuBszhV9vh3UJVq9HL0Lz
|
||||
40Bcjzcoob+Iza9KKnl0spLKMPgQNpoCerBCdE/v/DmiWus/gs2MOE+xE5dTM6A8kK0K4ukDoDjr
|
||||
mnkjezkK8iuh5wwjPqA=
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; s=mailing; d=srv2.de;
|
||||
h=Date:From:Reply-To:To:Message-ID:Subject:MIME-Version:Content-Type:X-ulpe:
|
||||
List-Id:X-CSA-Complaints:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID;
|
||||
bh=s/Rjfns4gt9JjcDSSsqHZctvWTOtocDJRpEVs80pElM=;
|
||||
b=UqUBlHLFoluhBzwmQDgHdd9OdiyI9Cy8Y5zqJqfyhmdV34Owpvu1Vx7HnlljqxlUTSVSPtL6Ldoe
|
||||
bjWHA8yBc0lFKnF7Kt8a2Wd2ac0aHsLgQvwVmoM0T9Av8Hgx4qyRhaTQIho2IbcKcP0IEwEUKVou
|
||||
KwU4tfT8MLuZHX4rkWc=
|
||||
Date: Mon, 12 Jul 2021 18:00:54 +0200 (CEST)
|
||||
From: =?UTF-8?Q?TT-Line_-_Die_Schwedenf=C3=A4hren?= <newsletter@ttline.com>
|
||||
Reply-To:
|
||||
=?UTF-8?Q?TT-Line_-_Die_Schwedenf=C3=A4hren?= <newsletter@ttline.com>
|
||||
To: pdetersen@b123.com
|
||||
Message-ID: <re-p123123123123123123123123-41231231-4J123123-1123123@t.ttline.com>
|
||||
Subject: =?UTF-8?Q?Unsere_Sommerangebote_an_Bord_=E2=9A=93?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----=_Part_15756440_1404911700.1626105654088"
|
||||
X-ulpe: re-p123123123123123123123123-41231231-4J123123-1123123@t.ttline.com
|
||||
List-Id: <39123123-1BBQXPY.t.ttline.com>
|
||||
X-Report-Spam: complaints@episerver.com
|
||||
X-CSA-Complaints: csa-complaints@eco.de
|
||||
X-sender: =?UTF-8?Q?TT-Line_-_Die_Schwedenf=C3=A4hren?= <newsletter@ttline.com>
|
||||
List-Unsubscribe: <mailto:listoff-41231231-4J123123-5M123@t.ttline.com?subject=unsubscribe>,<https://t.ttline.com/go/0/41231231-4J123123-39123123-A9E123-UL.html>
|
||||
List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
||||
Feedback-ID: 39123123:4J123123:episerver
|
||||
X-KasLoop: m123123f
|
||||
|
||||
------=_Part_15756440_1404911700.1626105654088
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
|
||||
plain...
|
||||
------=_Part_15756440_1404911700.1626105654088
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Disposition: inline
|
||||
|
||||
html...
|
||||
------=_Part_15756440_1404911700.1626105654088--
|
||||
|
||||
75
test-data/message/mailinglist_xing.eml
Normal file
75
test-data/message/mailinglist_xing.eml
Normal file
@@ -0,0 +1,75 @@
|
||||
Return-Path: <mailrobot@mail.xing.com>
|
||||
X-Original-To: pbetersen@b123.com
|
||||
Delivered-To: m123123f@dd12312.kasserver.com
|
||||
X-policyd-weight: using cached result; rate: -7
|
||||
Authentication-Results: dd12312.kasserver.com;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=mail.xing.com header.i=@mail.xing.com header.b="o123123j";
|
||||
dkim-atps=neutral
|
||||
Received: from mailout1-107.xing.com (mailout1-107.xing.com [109.233.158.107])
|
||||
by dd12312.kasserver.com (Postfix) with ESMTPS id DCB9D53C055C
|
||||
for <pbetersen@b123.com>; Tue, 14 Sep 2021 12:11:17 +0200 (CEST)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; d=mail.xing.com; s=main; c=relaxed/simple;
|
||||
q=dns/txt; i=@mail.xing.com; t=1631614277;
|
||||
h=From:Subject:Date:To:Mime-Version:Content-Type:X-CSA-Complaints:List-Help;
|
||||
bh=bc0sqvXhKYO4cLpVn9ZYqLQQYPgPysrjt9/f6mx2BQI=;
|
||||
b=o123123jtfoTCEiqaRZ3ax7h17rGfXP1ZQ3sjbdaBguPy5q1k+EpuXYdCwFq7S7z
|
||||
yRjbK+VvTSKDn4Dxqk/wA9hFyGrO6XuYdJt3NEZ3Yye7W222dNR58ww3XCavjSpY
|
||||
pzhJdAEo9Zw1sG3fhtm2eI60Oe1hLOr6G657sAo2ubQ=;
|
||||
X-MSFBL: SwzhdcKRUcljkfTYaEiSU28Q3a0pxO+Z9dY1NMgWyq4=|eyJnIjoibWFpbG91dDE
|
||||
iLCJiIjoibWFpbG91dDEtMTA3IiwidSI6ImNvbnRhY3RzL215bWs7RzhMWjI0RlR
|
||||
SSUItdVkxaXRaa1EiLCJyIjoiYnBldGVyc2VuQGI0NHQuY29tIn0=
|
||||
Received: from [10.12.225.241] ([10.12.225.241:27696])
|
||||
by mta-6.mail.ams1.xing.com (envelope-from <mailrobot@mail.xing.com>)
|
||||
(ecelerity 4.2.1.51128 r(Core:4.2.1.5)) with REST
|
||||
id 82/69-03094-54570416; Tue, 14 Sep 2021 12:11:17 +0200
|
||||
Date: Tue, 14 Sep 2021 12:11:17 +0200
|
||||
From: =?UTF-8?B?WElORyBLb250YWt0dm9yc2NobMOkZ2U=?= <mailrobot@mail.xing.com>
|
||||
Reply-To: no-reply@mail.xing.com
|
||||
To: =?UTF-8?B?QmrDtnJuIFBldGVyc2Vu?= <pbetersen@b123.com>
|
||||
Message-ID: <6123123123123_5123123123123@hermes-worker-5123123123-7lcmp.mail>
|
||||
Subject: Kennst Du Dr. Mabuse?
|
||||
Mime-Version: 1.0
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="--==_mimepart_614075452ac33_5ed3cc13386fc";
|
||||
charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
List-Help: <https://www.xing.com/settings/notifications>,
|
||||
<mailto:fbl@xing.com>
|
||||
List-Id: <51231231231231231231231232869f58.xing.com>
|
||||
X-CSA-Complaints: csa-complaints@eco.de
|
||||
X-KasLoop: m123123f
|
||||
|
||||
|
||||
----==_mimepart_614075452ac33_5ed3cc13386fc
|
||||
Content-Type: text/plain;
|
||||
charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
**********************************************************
|
||||
* unfortunately:
|
||||
*
|
||||
* - xing mailinglists do not have a name in `List-Id:`
|
||||
* so we cannot get the name from there
|
||||
*
|
||||
* - different senders may use the same `List-Id:`,
|
||||
* at least i found that two times,
|
||||
* maybe it is a bug on xing's side as most times `List-Id:` differs,
|
||||
* however, so we cannot get the name from `From:`.
|
||||
*
|
||||
* to avoid chat names as `51231231231231231231231232869f58.xing.com`,
|
||||
* we detect the hash prefix and strip that;
|
||||
* as the sender, we have "xing.com" then, which is fine.
|
||||
*
|
||||
* that approach should also work for other mailinglist.
|
||||
**********************************************************
|
||||
|
||||
|
||||
----==_mimepart_614075452ac33_5ed3cc13386fc
|
||||
Content-Type: text/html;
|
||||
charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
html ...
|
||||
|
||||
----==_mimepart_614075452ac33_5ed3cc13386fc--
|
||||
|
||||
164
test-data/message/mailinglist_xt_local_microsoft.eml
Normal file
164
test-data/message/mailinglist_xt_local_microsoft.eml
Normal file
@@ -0,0 +1,164 @@
|
||||
Return-Path: <bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com>
|
||||
X-Original-To: me@foobar.com
|
||||
Delivered-To: m111111f@dd22222.kasserver.com
|
||||
X-policyd-weight: NOT_IN_SPAMCOP=-1.5 NOT_IN_IX_MANITU=-1.5 CL_IP_EQ_HELO_MX=-3.1 (check from: .microsoftstoreemail. - helo: .merlinux. - helo-domain: .merlinux.) FROM/MX_MATCHES_NOT_HELO(DOMAIN)=1; rate: -5.1
|
||||
Authentication-Results: dd22222.kasserver.com;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=microsoftstoreemail.com header.i=microsoftstore@microsoftstoreemail.com header.b="XV8jLprF";
|
||||
dkim-atps=neutral
|
||||
Received: from merlin.eu (hq6.merlin.eu [95.217.159.152])
|
||||
by dd22222.kasserver.com (Postfix) with ESMTPS id BAD2E53C0541
|
||||
for <me@foobar.com>; Mon, 30 Aug 2021 20:29:57 +0200 (CEST)
|
||||
Received: from [127.0.0.1] (localhost [127.0.0.1])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by merlin.eu (Postfix) with ESMTPS id 40DA541C7E
|
||||
for <me@merlin.eu>; Mon, 30 Aug 2021 20:29:55 +0200 (CEST)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=200608; d=microsoftstoreemail.com;
|
||||
h=From:To:Subject:Date:List-Help:MIME-Version:List-ID:X-CSA-Complaints:
|
||||
Message-ID:Content-Type; i=microsoftstore@microsoftstoreemail.com;
|
||||
bh=dvrsbCk+3USZNtvsQRvPSo2qpqsG0det56Snu0/Vz7I=;
|
||||
b=XV8jLprFcPv/OmruNBYNRrau26cDZYl4EchN88fJa3q49VpWwom5Pakcw2fkj1i63acBRpyVOBEr
|
||||
M3rZ/p/S3c+n5wkqcQJO/ruWPR16GacnfwYq3zGFIEs5HVjFLbMF+26YuCD7u6GEJC559yD7kWje
|
||||
1RCh9UZqYxBOsdYhkyk=
|
||||
Received: by mta16.microsoftstoreemail.com id h1111111111v for <me@merlin.eu>; Mon, 30 Aug 2021 18:14:50 +0000 (envelope-from <bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com>)
|
||||
From: "Microsoft Store" <microsoftstore@microsoftstoreemail.com>
|
||||
To: <me@merlin.eu>
|
||||
Subject: Notice of Update to Microsoft Store Marketing Disclosure Document
|
||||
Date: Mon, 30 Aug 2021 12:14:50 -0600
|
||||
List-Help: <https://click.microsoftstoreemail.com/subscription_center.aspx?jwt=123.123.123>
|
||||
x-CSA-Compliance-Source: SFMC
|
||||
MIME-Version: 1.0
|
||||
List-ID: <96540.xt.local>
|
||||
X-CSA-Complaints: csa-complaints@eco.de
|
||||
X-SFMC-Stack: 1
|
||||
x-job: 10359607_7641603
|
||||
Message-ID: <9b806dd3-1234-1234-1234-49603f588b2c@ind1s01mta720.xt.local>
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="x28u2yBkExvV=_?:"
|
||||
X-Spamd-Bar: ++++
|
||||
X-Spam-Level: ****
|
||||
X-Rspamd-Server: hq6
|
||||
Authentication-Results: merlin.eu;
|
||||
dkim=pass header.d=microsoftstoreemail.com;
|
||||
dmarc=pass (policy=none) header.from=microsoftstoreemail.com;
|
||||
spf=pass smtp.mailfrom=bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com
|
||||
X-Rspamd-Queue-Id: 40DA541C7E
|
||||
X-Spamd-Result: default: False [4.17 / 15.00];
|
||||
ARC_NA(0.00)[];
|
||||
R_DKIM_ALLOW(-0.20)[microsoftstoreemail.com];
|
||||
FROM_HAS_DN(0.00)[];
|
||||
R_SPF_ALLOW(-0.20)[+ip4:64.132.88.0/23];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
R_BAD_CTE_7BIT(1.05)[7bit,utf8];
|
||||
PREVIOUSLY_DELIVERED(0.00)[me@merlin.eu];
|
||||
TO_DN_NONE(0.00)[];
|
||||
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
|
||||
URI_COUNT_ODD(1.00)[63];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
MANY_INVISIBLE_PARTS(0.10)[2];
|
||||
IP_SCORE(-0.47)[asn: 22606(-2.23), country: US(-0.10)];
|
||||
DKIM_TRACE(0.00)[microsoftstoreemail.com:+];
|
||||
HTML_SHORT_LINK_IMG_2(1.00)[];
|
||||
DMARC_POLICY_ALLOW(-0.50)[microsoftstoreemail.com,none];
|
||||
MX_GOOD(-0.01)[cached: inbound.s1.exacttarget.com];
|
||||
FORGED_SENDER(0.30)[microsoftstore@microsoftstoreemail.com,bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com];
|
||||
RWL_MAILSPIKE_POSSIBLE(0.00)[177.89.132.64.rep.mailspike.net : 127.0.0.17];
|
||||
RCVD_TLS_LAST(0.00)[];
|
||||
HFILTER_URL_ONLY(2.20)[1];
|
||||
ASN(0.00)[asn:22606, ipnet:64.132.89.0/24, country:US];
|
||||
FROM_NEQ_ENVFROM(0.00)[microsoftstore@microsoftstoreemail.com,bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com];
|
||||
GREYLIST(0.00)[pass,body];
|
||||
RCVD_COUNT_TWO(0.00)[2]
|
||||
X-KasLoop: m111111f
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--x28u2yBkExvV=_?:
|
||||
Content-Type: text/plain;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
en_us
|
||||
|
||||
@templateVersion = "Templates_v01"
|
||||
-->
|
||||
<span style="display: none"></span>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Notice of Update to Microsoft Store Marketing Disclosure Document</title>
|
||||
<style media="all" type="text/css">
|
||||
|
||||
div.preheader {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Wrapper -->
|
||||
<div style="display: none; max-height: 0px; overflow: hidden;">Please read these important updates ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
|
||||
... 100 or so more of these lines - and yes, this is really in a `text/plain` part ...
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
--x28u2yBkExvV=_?:
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
|
||||
<!-- Template Top -->
|
||||
<!--
|
||||
en_us
|
||||
|
||||
@templateVersion = "Templates_v01"
|
||||
-->
|
||||
<span style="display: none"></span>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Notice of Update to Microsoft Store Marketing Disclosure Document</title>
|
||||
<style media="all" type="text/css">
|
||||
|
||||
div.preheader {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Wrapper -->
|
||||
<div style="display: none; max-height: 0px; overflow: hidden;">Please read these important updates ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
|
||||
|
||||
... 100 or so more of these lines ...
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
--x28u2yBkExvV=_?:--
|
||||
|
||||
70
test-data/message/mailinglist_xt_local_spiegel.eml
Normal file
70
test-data/message/mailinglist_xt_local_spiegel.eml
Normal file
@@ -0,0 +1,70 @@
|
||||
Return-Path: <bounce-155_HTML-111111111-222222-333333333-1234@bounce.angebote.spiegel.de>
|
||||
X-Original-To: me@foobar.com
|
||||
Delivered-To: m000002f@dd22222.kasserver.com
|
||||
X-policyd-weight: NOT_IN_SPAMCOP=-1.5 NOT_IN_IX_MANITU=-1.5 CL_IP_EQ_HELO_IP=-2 (check from: .spiegel. - helo: .mta.angebote.spiegel. - helo-domain: .spiegel.) FROM/MX_MATCHES_HELO(DOMAIN)=-2; rate: -7
|
||||
Authentication-Results: dd22222.kasserver.com;
|
||||
dkim=pass (2048-bit key; unprotected) header.d=angebote.spiegel.de header.i=service@angebote.spiegel.de header.b="EO7/nr7w";
|
||||
dkim-atps=neutral
|
||||
Received: from mta.angebote.spiegel.de (mta.angebote.spiegel.de [13.111.60.76])
|
||||
by dd22222.kasserver.com (Postfix) with ESMTPS id 5667B53C0576
|
||||
for <me@foobar.com>; Mon, 6 Sep 2021 13:27:43 +0200 (CEST)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=10dkim1; d=angebote.spiegel.de;
|
||||
h=From:To:Subject:Date:List-Unsubscribe:List-Unsubscribe-Post:MIME-Version:
|
||||
Reply-To:List-ID:X-CSA-Complaints:Message-ID:Content-Type;
|
||||
i=service@angebote.spiegel.de;
|
||||
bh=OgZoChKe0M5AaHNMVEyCAZ9jK0q01BajqQhtcqP22/Q=;
|
||||
b=EO7/nr7w54xd68XV1+qUteycqAN63r7HH4QAw60wImD4rE76J4+vWAcf8TNYayM/vPefcM7zcfFk
|
||||
ERwrlR/aT4BoRAWghyQHKgAZ0lwVKpqWGuYe9cF8wjLuYJOwPCoYIiry/GgOSXIVwazlXE2FRN9V
|
||||
Y8m8OStmH3KbVZC55j1Ta6OXMMHEyvwZ+/OHbJ2CLW3jinw84NevP2aMDoI60TidBS5HYVclUT9W
|
||||
IT4bs1o6a659LeVw/ViitQULL7c2P/UWPv0gm5w5IRci6jmdCOzfa+rvFmxSGIlfalTJ/VVG4V+V
|
||||
PlbJl49RrPBuXl62ub6f1EPjFGtbJD8Gy3BliQ==
|
||||
Received: by mta.angebote.spiegel.de id h6ntj42fmd4o for <me@foobar.com>; Mon, 6 Sep 2021 11:27:41 +0000 (envelope-from <bounce-155_HTML-111111111-222222-333333333-1234@bounce.angebote.spiegel.de>)
|
||||
From: "DER SPIEGEL Kundenservice" <service@angebote.spiegel.de>
|
||||
To: <me@foobar.com>
|
||||
Subject: subject here
|
||||
Date: Mon, 06 Sep 2021 05:27:41 -0600
|
||||
List-Unsubscribe: <https://click.angebote.spiegel.de/subscription_center.aspx?jwt=123.123.123-123-123>, <mailto:leave-123-123-123-123-123@leave.angebote.spiegel.de>
|
||||
List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
||||
x-CSA-Compliance-Source: SFMC
|
||||
MIME-Version: 1.0
|
||||
Reply-To: "DER SPIEGEL Kundenservice" <reply-123-123-123-123-123@angebote.spiegel.de>
|
||||
List-ID: <121231234.xt.local>
|
||||
X-CSA-Complaints: csa-complaints@eco.de
|
||||
X-SFMC-Stack: 10
|
||||
x-job: 100006074_884883
|
||||
Message-ID: <123-123-123-123-123@dfw123123a96.xt.local>
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="NwnF3UJtimpn=_?:"
|
||||
X-KasLoop: m000002f
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--NwnF3UJtimpn=_?:
|
||||
Content-Type: text/plain;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
plain text here
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
--NwnF3UJtimpn=_?:
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
<p>html text here</p>
|
||||
|
||||
--NwnF3UJtimpn=_?:--
|
||||
|
||||
97
test-data/message/testrun_ndn_2.eml
Normal file
97
test-data/message/testrun_ndn_2.eml
Normal file
@@ -0,0 +1,97 @@
|
||||
Return-Path: <>
|
||||
Delivered-To: alice@example.org
|
||||
Received: from hq5.merlinux.eu
|
||||
by hq5.merlinux.eu with LMTP
|
||||
id IF/+LHrTFGEQBQAAPzvFDg
|
||||
(envelope-from <>)
|
||||
for <alice@example.org>; Thu, 12 Aug 2021 09:53:30 +0200
|
||||
Received: by hq5.merlinux.eu (Postfix)
|
||||
id 9C87727A0006; Thu, 12 Aug 2021 09:53:30 +0200 (CEST)
|
||||
Date: Thu, 12 Aug 2021 09:53:30 +0200 (CEST)
|
||||
From: MAILER-DAEMON@hq5.merlinux.eu (Mail Delivery System)
|
||||
Subject: Undelivered Mail Returned to Sender
|
||||
To: alice@example.org
|
||||
Auto-Submitted: auto-replied
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=delivery-status;
|
||||
boundary="A82D727A0003.1628754810/hq5.merlinux.eu"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Message-Id: <20210812075330.9C87727A0006@hq5.merlinux.eu>
|
||||
|
||||
This is a MIME-encapsulated message.
|
||||
|
||||
--A82D727A0003.1628754810/hq5.merlinux.eu
|
||||
Content-Description: Notification
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the mail system at host hq5.merlinux.eu.
|
||||
|
||||
I'm sorry to have to inform you that your message could not
|
||||
be delivered to one or more recipients. It's attached below.
|
||||
|
||||
For further assistance, please send mail to postmaster.
|
||||
|
||||
If you do so, please include this problem report. You can
|
||||
delete your own text from the attached returned message.
|
||||
|
||||
The mail system
|
||||
|
||||
<bob@example.org>: Host or domain name not found. Name service error for
|
||||
name=echedelyr.tk type=AAAA: Host not found
|
||||
|
||||
--A82D727A0003.1628754810/hq5.merlinux.eu
|
||||
Content-Description: Delivery report
|
||||
Content-Type: message/global-delivery-status
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Reporting-MTA: dns; hq5.merlinux.eu
|
||||
X-Postfix-Queue-ID: A82D727A0003
|
||||
X-Postfix-Sender: rfc822; alice@example.org
|
||||
Arrival-Date: Thu, 12 Aug 2021 09:53:29 +0200 (CEST)
|
||||
|
||||
Final-Recipient: rfc822; bob@example.org
|
||||
Original-Recipient: rfc822;bob@example.org
|
||||
Action: failed
|
||||
Status: 5.4.4
|
||||
Diagnostic-Code: X-Postfix; Host or domain name not found. Name service error
|
||||
for name=echedelyr.tk type=AAAA: Host not found
|
||||
|
||||
--A82D727A0003.1628754810/hq5.merlinux.eu
|
||||
Content-Description: Undelivered Message Headers
|
||||
Content-Type: message/global-headers
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Return-Path: <alice@example.org>
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=testrun.org;
|
||||
s=testrun; t=1628754810;
|
||||
bh=exDToKrRerWMZ62UQVK/RgNiwDDKe+GqF6zGdG64jl8=;
|
||||
h=Subject:References:Date:To:From:From;
|
||||
b=SNTTDdhkppXqimCSPP+cqDvdzmryYwzurtZdN2XkTVQEeqMMdnGvEA9TOgeZpHqi0
|
||||
oHSjJ5oD+eUK1ECnfRuxDF9DmFnK7sbw1MaHxIcAVTLPgHrMxv+2Fjq1nmrerzmr1t
|
||||
z9jOYY8e6gfEBw1uDAfHMmIl4OGuoDSll8haqNF3C2JqdwTtcdtE/w6ERJwSeHhCsR
|
||||
bknan9Rh75Tr46Zh8WVi9YYRVDGFj7+OlL/67Va+Jxl3c4v4EJ5vF6ncxyJupP9eU2
|
||||
qndV+g1BbxsARo663codYZRiGh217AI8DG2HUr0rVOPdvWm1kw/NTkp3BxoHkv5q2a
|
||||
Uak5Jiieur6Hg==
|
||||
Subject: Message from Hocuri
|
||||
Chat-User-Avatar: 0
|
||||
MIME-Version: 1.0
|
||||
References: <Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org>
|
||||
Date: Thu, 12 Aug 2021 07:53:28 +0000
|
||||
Chat-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org;
|
||||
keydata=xjMEX3tmZxYJKwYBBAHaRw8BAQdAl4LKVKPRqxG1ZXEO8e9s1DZWt6f38wSuJnY0mLSOuf
|
||||
7NFTxob2N1cmkxQHRlc3RydW4ub3JnPsKLBBAWCAAzAhkBBQJfe2ZnAhsDBAsJCAcGFQgJCgsCAxYC
|
||||
ARYhBBmltZLxgqC0SHJPEL7qvlxmQUTNAAoJEL7qvlxmQUTNIucBAJkRclHRG7cWpFbMYW+rspEFIQ
|
||||
j1GTKwriiBpk5ffnroAQC3h/scScpG/EeIPL0y80GRS5BoR1Ium3zrlR92EaijDc44BF97ZmcSCisG
|
||||
AQQBl1UBBQEBB0CmxhyX/NuXIlrl0/fdeEseAv6KCbZ4tV3tIvSvnH1KHgMBCAfCeAQYFggAIAUCX3
|
||||
tmZwIbDBYhBBmltZLxgqC0SHJPEL7qvlxmQUTNAAoJEL7qvlxmQUTNnkYA/3qY+e6PrtR1WT7PiVeZ
|
||||
RIQBkkJjWWSx+lBQ5fNb3e92AQCBEG3OnGy4RrxOqWW2ry7ETP33CJeiwAwCvv4LQlwCCw==
|
||||
Message-ID: <Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org>
|
||||
To: <bob@example.org>
|
||||
From: Hocuri <alice@example.org>
|
||||
Content-Type: multipart/mixed; boundary="qtFe0wPDNHWVlvqV0B8ymdWmE6ZmKD"
|
||||
|
||||
--A82D727A0003.1628754810/hq5.merlinux.eu--
|
||||
|
||||
Reference in New Issue
Block a user