mirror of
https://github.com/chatmail/core.git
synced 2026-07-11 21:43:25 +03:00
Compare commits
54 Commits
1.0.0-beta
...
refactor-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a32d29574 | ||
|
|
15bf53c092 | ||
|
|
f6afd5f7f1 | ||
|
|
515f0c5089 | ||
|
|
5a11551b4d | ||
|
|
49bf99588b | ||
|
|
231110fb61 | ||
|
|
4c30bf80ce | ||
|
|
f8afefa2c1 | ||
|
|
89bb2d0ffe | ||
|
|
b5d5d98645 | ||
|
|
89f394ab86 | ||
|
|
cbaa4e03b3 | ||
|
|
50539465b9 | ||
|
|
be08bcb22b | ||
|
|
dcd92a894e | ||
|
|
6336eeb568 | ||
|
|
6b18cbda1f | ||
|
|
cf023ea557 | ||
|
|
51a804a80f | ||
|
|
1a33b1c574 | ||
|
|
67e2e4d415 | ||
|
|
8c2efa707a | ||
|
|
87abc6e4a2 | ||
|
|
0ea017c53d | ||
|
|
b9c7510b58 | ||
|
|
01e7caf65a | ||
|
|
1cfeb730c3 | ||
|
|
a3b90a08b6 | ||
|
|
31571be71e | ||
|
|
661fc45106 | ||
|
|
da64dee3e0 | ||
|
|
cb00f5da79 | ||
|
|
e1df41c209 | ||
|
|
3b64748427 | ||
|
|
70cef68eeb | ||
|
|
c5f64d2988 | ||
|
|
4eb068613d | ||
|
|
d774430ec2 | ||
|
|
d24a982757 | ||
|
|
d74c70a57c | ||
|
|
a6f0f78588 | ||
|
|
e6d9991581 | ||
|
|
ec8dbddcfb | ||
|
|
bc699f17d9 | ||
|
|
832df41130 | ||
|
|
5709681076 | ||
|
|
858baf0c2c | ||
|
|
e4b3e23769 | ||
|
|
8ce05796da | ||
|
|
7f8c6d8cca | ||
|
|
75ba040531 | ||
|
|
faa78e1c04 | ||
|
|
b264d3be3c |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0-beta.7
|
||||
|
||||
- fix location-streaming #782
|
||||
|
||||
- fix display of messages that could not be decrypted #785
|
||||
|
||||
- fix smtp MAILER-DAEMON bug #786
|
||||
|
||||
- fix a logging of durations #783
|
||||
|
||||
- add more error logging #779
|
||||
|
||||
- do not panic on some bad utf-8 mime #776
|
||||
|
||||
## 1.0.0-beta.6
|
||||
|
||||
- fix chatlist.get_msg_id to return id, instead of wrongly erroring
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -480,7 +480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.6"
|
||||
version = "1.0.0-beta.7"
|
||||
dependencies = [
|
||||
"backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -550,9 +550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.0.0-beta.6"
|
||||
version = "1.0.0-beta.7"
|
||||
dependencies = [
|
||||
"deltachat 1.0.0-beta.6",
|
||||
"deltachat 1.0.0-beta.7",
|
||||
"deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"human-panic 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.0.0-beta.6"
|
||||
version = "1.0.0-beta.7"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
license = "MPL"
|
||||
|
||||
BIN
assets/icon-device.png
Normal file
BIN
assets/icon-device.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -43,8 +43,8 @@ if [ -n "$TESTS" ]; then
|
||||
# we split out qr-tests run to minimize likelyness of flaky tests
|
||||
# (some qr tests are pretty heavy in terms of send/received
|
||||
# messages and rust's imap code likely has concurrency problems)
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- -k "not qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- -k "qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||
unset DCC_PY_LIVECONFIG
|
||||
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.0.0-beta.6"
|
||||
version = "1.0.0-beta.7"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -338,6 +338,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `send_pw` = SMTP-password, guessed if left out
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way eg. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display eg. in email footers, defaults to a standard text
|
||||
* - `selfavatar` = File containing avatar. Will be copied to blob directory.
|
||||
@@ -1096,6 +1098,27 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
void dc_set_draft (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Add a message to the device-chat.
|
||||
* Device-messages usually contain update information
|
||||
* and some hints that are added during the program runs, multi-device etc.
|
||||
*
|
||||
* Device-messages may be added from the core,
|
||||
* however, with this function, this can be done from the ui as well.
|
||||
* If needed, the device-chat is created before.
|
||||
*
|
||||
* Sends the event #DC_EVENT_MSGS_CHANGED on success.
|
||||
* To check, if a given chat is a device-chat, see dc_chat_is_device_talk()
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param msg Message to be added to the device-chat.
|
||||
* The message appears to the user as an incoming message.
|
||||
* @return The ID of the added message.
|
||||
*/
|
||||
uint32_t dc_add_device_msg (dc_context_t* context, dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get draft for a chat, if any.
|
||||
* See dc_set_draft() for more details about drafts.
|
||||
@@ -2610,8 +2633,6 @@ int dc_chat_get_type (const dc_chat_t* chat);
|
||||
*
|
||||
* To change the name, use dc_set_chat_name()
|
||||
*
|
||||
* See also: dc_chat_get_subtitle()
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return Chat name as a string. Must be released using dc_str_unref() after usage. Never NULL.
|
||||
@@ -2619,13 +2640,13 @@ int dc_chat_get_type (const dc_chat_t* chat);
|
||||
char* dc_chat_get_name (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
/*
|
||||
* Get a subtitle for a chat. The subtitle is eg. the email-address or the
|
||||
* number of group members.
|
||||
*
|
||||
* See also: dc_chat_get_name()
|
||||
* Deprecated function. Subtitles should be created in the ui
|
||||
* where plural forms and other specials can be handled more gracefully.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object to calulate the subtitle for.
|
||||
* @return Subtitle as a string. Must be released using dc_str_unref() after usage. Never NULL.
|
||||
*/
|
||||
@@ -2715,6 +2736,39 @@ int dc_chat_is_unpromoted (const dc_chat_t* chat);
|
||||
int dc_chat_is_self_talk (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is a device-talk.
|
||||
* Device-talks contain update information
|
||||
* and some hints that are added during the program runs, multi-device etc.
|
||||
*
|
||||
* From the ui view, device-talks are not very special,
|
||||
* the user can delete and forward messages, archive the chat, set notifications etc.
|
||||
*
|
||||
* Messages may be added from the core to the device chat,
|
||||
* so the chat just pops up as usual.
|
||||
* However, if needed the ui can also add messages using dc_add_device_msg()
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat is device-talk, 0=chat is no device-talk
|
||||
*/
|
||||
int dc_chat_is_device_talk (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if messages can be sent to a give chat.
|
||||
* This is not true eg. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
|
||||
*
|
||||
* Calling dc_send_msg() for these chats will fail
|
||||
* and the ui may decide to hide input controls therefore.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat is writable, 0=chat is not writable
|
||||
*/
|
||||
int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a chat is verified. Verified chats contain only verified members
|
||||
* and encryption is alwasy enabled. Verified chats are created using
|
||||
@@ -3370,7 +3424,8 @@ void dc_msg_latefiling_mediasize (dc_msg_t* msg, int width, int hei
|
||||
|
||||
|
||||
#define DC_CONTACT_ID_SELF 1
|
||||
#define DC_CONTACT_ID_DEVICE 2
|
||||
#define DC_CONTACT_ID_INFO 2 // centered messages as "member added", used in all chats
|
||||
#define DC_CONTACT_ID_DEVICE 5 // messages "update info" in the device-chat
|
||||
#define DC_CONTACT_ID_LAST_SPECIAL 9
|
||||
|
||||
|
||||
@@ -3894,7 +3949,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*
|
||||
* These constants configure TLS certificate checks for IMAP and SMTP connections.
|
||||
*
|
||||
* These constants are set via dc_set_config
|
||||
* These constants are set via dc_set_config()
|
||||
* using keys "imap_certificate_checks" and "smtp_certificate_checks".
|
||||
*
|
||||
* @addtogroup DC_CERTCK
|
||||
@@ -3907,8 +3962,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CERTCK_AUTO 0
|
||||
|
||||
/**
|
||||
* Strictly check TLS certificates.
|
||||
* Require that both the certificate and hostname are valid.
|
||||
* Strictly check TLS certificates;
|
||||
* require that both the certificate and hostname are valid.
|
||||
*/
|
||||
#define DC_CERTCK_STRICT 1
|
||||
|
||||
|
||||
@@ -811,6 +811,23 @@ pub unsafe extern "C" fn dc_set_draft(
|
||||
.unwrap_or(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_add_device_msg(context: *mut dc_context_t, msg: *mut dc_msg_t) -> u32 {
|
||||
if context.is_null() || msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_add_device_msg()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_context = &mut *context;
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_context
|
||||
.with_inner(|ctx| {
|
||||
chat::add_device_msg(ctx, &mut ffi_msg.message)
|
||||
.unwrap_or_log_default(ctx, "Failed to add device message")
|
||||
})
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32) -> *mut dc_msg_t {
|
||||
if context.is_null() {
|
||||
@@ -2264,6 +2281,26 @@ pub unsafe extern "C" fn dc_chat_is_self_talk(chat: *mut dc_chat_t) -> libc::c_i
|
||||
ffi_chat.chat.is_self_talk() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_device_talk(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_device_talk()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.is_device_talk() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_can_send()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.can_send() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_verified(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
|
||||
@@ -2,7 +2,7 @@ extern crate deltachat_provider_database;
|
||||
|
||||
use std::ptr;
|
||||
|
||||
use deltachat::dc_tools::{as_str, StrExt};
|
||||
use deltachat::dc_tools::{to_string_lossy, StrExt};
|
||||
use deltachat_provider_database::StatusState;
|
||||
|
||||
#[no_mangle]
|
||||
@@ -12,7 +12,7 @@ pub type dc_provider_t = deltachat_provider_database::Provider;
|
||||
pub unsafe extern "C" fn dc_provider_new_from_domain(
|
||||
domain: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
match deltachat_provider_database::get_provider_info(as_str(domain)) {
|
||||
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null(),
|
||||
}
|
||||
@@ -22,7 +22,8 @@ pub unsafe extern "C" fn dc_provider_new_from_domain(
|
||||
pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
email: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
let domain = deltachat_provider_database::get_domain_from_email(as_str(email));
|
||||
let email = to_string_lossy(email);
|
||||
let domain = deltachat_provider_database::get_domain_from_email(&email);
|
||||
match deltachat_provider_database::get_provider_info(domain) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null(),
|
||||
|
||||
@@ -353,6 +353,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
interrupt\n\
|
||||
maybenetwork\n\
|
||||
housekeeping\n\
|
||||
help imex (Import/Export)\n\
|
||||
@@ -378,6 +379,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
sendimage <file> [<text>]\n\
|
||||
sendfile <file> [<text>]\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
listmedia\n\
|
||||
archive <chat-id>\n\
|
||||
unarchive <chat-id>\n\
|
||||
@@ -493,6 +495,9 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
"info" => {
|
||||
println!("{:#?}", context.get_info());
|
||||
}
|
||||
"interrupt" => {
|
||||
interrupt_imap_idle(context);
|
||||
}
|
||||
"maybenetwork" => {
|
||||
maybe_network(context);
|
||||
}
|
||||
@@ -517,15 +522,12 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
|
||||
let temp_subtitle = chat.get_subtitle(context);
|
||||
let temp_name = chat.get_name();
|
||||
info!(
|
||||
context,
|
||||
"{}#{}: {} [{}] [{} fresh]",
|
||||
"{}#{}: {} [{} fresh]",
|
||||
chat_prefix(&chat),
|
||||
chat.get_id(),
|
||||
temp_name,
|
||||
temp_subtitle,
|
||||
chat.get_name(),
|
||||
chat::get_fresh_msg_cnt(context, chat.get_id()),
|
||||
);
|
||||
let lot = chatlist.get_summary(context, i, Some(&chat));
|
||||
@@ -583,20 +585,34 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
let sel_chat = sel_chat.as_ref().unwrap();
|
||||
|
||||
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None);
|
||||
let temp2 = sel_chat.get_subtitle(context);
|
||||
let temp_name = sel_chat.get_name();
|
||||
let members = chat::get_chat_contacts(context, sel_chat.id);
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
} else if sel_chat.get_type() == Chattype::Single && members.len() >= 1 {
|
||||
let contact = Contact::get_by_id(context, members[0])?;
|
||||
contact.get_addr().to_string()
|
||||
} else {
|
||||
format!("{} member(s)", members.len())
|
||||
};
|
||||
info!(
|
||||
context,
|
||||
"{}#{}: {} [{}]{}",
|
||||
"{}#{}: {} [{}]{}{}",
|
||||
chat_prefix(sel_chat),
|
||||
sel_chat.get_id(),
|
||||
temp_name,
|
||||
temp2,
|
||||
sel_chat.get_name(),
|
||||
subtitle,
|
||||
if sel_chat.is_sending_locations() {
|
||||
"📍"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
match sel_chat.get_profile_image(context) {
|
||||
Some(icon) => match icon.to_str() {
|
||||
Some(icon) => format!(" Icon: {}", icon),
|
||||
_ => " Icon: Err".to_string(),
|
||||
},
|
||||
_ => "".to_string(),
|
||||
},
|
||||
);
|
||||
log_msglist(context, &msglist)?;
|
||||
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
|
||||
@@ -814,6 +830,15 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
|
||||
println!("Draft deleted.");
|
||||
}
|
||||
}
|
||||
"devicemsg" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Please specify text to add as device message."
|
||||
);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(arg1.to_string()));
|
||||
chat::add_device_msg(context, &mut msg)?;
|
||||
}
|
||||
"listmedia" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
|
||||
7
proptest-regressions/dc_mimeparser.txt
Normal file
7
proptest-regressions/dc_mimeparser.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 03cab93c6d1f3a8245f63cf84dacb307944294fe6333c1e38f078a6600659c7a # shrinks to data = "a\t0aA\ta\t0 \ta\t0 \ta a\t\ta A\tAA0a0a 0\t a\t aA \t a\t A0\t AAa\taA0\taAAaA\t0\taa0a\ta Aa Aaaa A0A\t a aA 0\t A\t0\t0\t\t\t\t\t\tA \t\t a\tA Aa aAA0A0AA0aaA A\t\t aa0\ta\t \tAa\taA\t00 AA A a\tA a aAAa \t 00\t0 \t\t a A 0\t\t\t aAA Aa \taAAAA0a A0A\t\t1\\E$\t$R\tc\t^=\t\"\tQ<Uk\t\t>A\t\t&\t}v&\tM^\'|\tW5?dn\t\t+\t\tP\te`\t\t>:Brlq--?\t$#Q\tK=zJ\tb\"9*.\"\t`\tF&T*\tBs,\tg\'*\\\t:\t?l$\t\t|A\"HR:Hk\t\\KkV\t\t{=&!^e%:|_*0wV\t[$`\t:\t$%f\t\t[!\"Y. \tP\t\th\'?\'/?%:++NfQo#@\"+?\t(\\??{\t\'\'$Dzj\t0.?{s4?&?Y=/yj]Z=\t4n\t?Ja\"\t{I\t$\t;I}_8V\t&\t?N\'\tI2/\t9.\tFT%9%`\'\tz\to7Y\t|AXP&@G12g\t\'w\t\t%??\t\"h$?F\"\"6%q\\\\{\tT\t\"]$87$}h\'\t<\t$\tc%U:mT2:<v\t#Rl!;U\t\t\"^D\tRZ(BZ{n\t%[$fgH`\t{B}:*\t\t%%*F`%W\t//B}PQ\t\tsu2\tLz<1*!p-X\tnKv&&0\thm4<n\\.\\/.w\'\t<)E1g3#ood\t`?\t\\({N?,8_?h\ty\t0%\t*$A\t\t*w-ViQUj\tTiH\t%\t%&0p\'\'\tA%r**Fo\'Z\\\tNI*ely4=I?}$.o`\t$\ts\'<\t\",:~=Nv/0O%=+<LS\t%P\'\t$r%d.\t{G{/L:\t&I&8-`cy*\"{\t/%fP9.P<\t\t\'/`\t\t`\t\t`!t:\t::\t\tW\'X^\t@.uL&a\tN\t\t\t.\t?0*\tvUK>UJ\\\tQbj%w]P=Js\t\"R\t&s^?</=%\t\'VI:\" kT`{b*<\t\tF&?Y\t\t:/\t!C\'e0`\t\t\tx-\t*\\N\\wwu.I4\tb/y\t\"P:P%\"\t\tQ][\\\t^x\t\t):\t\t&s\t$1-\t\t\tXt`0\t;\t/UeCP*\"G\t\t\':\tk@91Hf&#\t(Uzz&`p2;\t{]\t\"I_%=\\%,XD\"\'06QfCd<;$:q^\t8:\"*\"H\t\to\t&xK/\t\ty0/<}<j<|*`~::b\t=S.=}=Ki\t<Y.\'{\tf\t{Ub*H%(_4*?*\tn2\t/\'\t\t\t/,4]\tt\t<y\t\t\tWi\t\tT&\"\t\t\t\t\t=/1Wu?\t\'A\"W-P\t$?e\\\t`\t6`vD\t8`\t\tccD J\tY&jI//?_\t\\j\t_\tsiq$\t?9\tQ\t.^%&..?%Jm??A%+<\tN&*\t.g\tS$W\"\"\tMpq\t\t:&\\\thTz&<iz%/%T&\'F\t\\]&\t\t}\t\t\tXr=dtp`:+<,\t%60Y*<(&K*kJ\todO\t=<V?&\tMU/\"\t= Y!)<\tV\t9\t)\t&v8q{\t\t&pT\t3\ttB,qcq\'i$;gv%j_%M_{[\"&;\t\t\t.B;6Y\\%\t\"\tY/a\t\\`:?\t<\t?]\taNwD;\\\t%l*74%5T?QS :~yR/E*R\t\t=u\t\\\t\t.Q<;\\\t_S/{&F$^\tw_>\'h=|%\t\t:W!\\<U2\'$\tb&`\t=|=L9\t\t\t\\WZ:! }{\t ;\t;\t\t 0.*\t.%\"\'r%{<Mh_t[[}\t-\tJo\"b/AC*-=`=T\tz$2\tC\t\t/k{47\"\t\t,q%\tZ\tT3\t\tf>%\t\'?%@^tx\t7\"1Bk{b{n\t\"Pj3\tHc\t\tt\tY<\t#?\tSh\\yk/N\\\t8 7=R4*9Cw&m\t\\-\'f\t|\'#t(Etu.Hdu(E\t%&v:\'aqW~A5\t\t w.s{J%lX<\"\t\'*%m<&:/B<&\':U}$&`.{)\t\t6S\t:/$*kQ-Z\t^\'t${/tH*\'v\t3\t=\t\tDyp:B\t`I_R&4SO\t\t&-j=*.\t87&\'e*( \t\t\t\'<$\\DJ<$p?{}\'&\tv\t\\Xk<Y:Y`!$K{\tF&\tzd\t\t*i$\tj\'\t<)R*\t%?\t!.\t=\"@#~:=*\t\tXO=_T,1\"\'.%%\"`{\\:\t\"\tfkeOb/\'$I~\ta\t|&\t[\\KK\"1&Z\t<k\t\t)%\'-~\"2n\tj\tW?*<@w{g%d\ta\\\'\'I\t;:ySR%ke:4\tc\t$=\t&9P]x4\tJ=\t6C6%a\t`0\tF\tm-\tTr\t}\t\tQum\t&@\typ|w2&\t\t3`i&t\t\tT5\"\t.&b&e*/==1.\'*\\[U*\tqPL%?$-0/}~|q`\t\t}\t$\tq==o+T$\'!H\t\ti&um\"?\"%%\t/\'p\tg>?{0{J{\t\t/\t\t{zKZ&>=\t[\"1h<H%z/8,/]s\tv{7\t\t:j*H,M//\t\t\td\'.)\t"
|
||||
@@ -8,8 +8,8 @@ high level API reference
|
||||
|
||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||
other classes)
|
||||
- :class:`deltachat.chatting.Contact`
|
||||
- :class:`deltachat.chatting.Chat`
|
||||
- :class:`deltachat.contact.Contact`
|
||||
- :class:`deltachat.chat.Chat`
|
||||
- :class:`deltachat.message.Message`
|
||||
|
||||
Account
|
||||
@@ -22,13 +22,13 @@ Account
|
||||
Contact
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.chatting.Contact
|
||||
.. autoclass:: deltachat.contact.Contact
|
||||
:members:
|
||||
|
||||
Chat
|
||||
----
|
||||
|
||||
.. autoclass:: deltachat.chatting.Chat
|
||||
.. autoclass:: deltachat.chat.Chat
|
||||
:members:
|
||||
|
||||
Message
|
||||
|
||||
@@ -15,12 +15,14 @@ import deltachat
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .chatting import Contact, Chat, Message
|
||||
from .chat import Chat
|
||||
from .message import Message
|
||||
from .contact import Contact
|
||||
|
||||
|
||||
class Account(object):
|
||||
""" Each account is tied to a sqlite database file which is fully managed
|
||||
by the underlying deltachat c-library. All public Account methods are
|
||||
by the underlying deltachat core library. All public Account methods are
|
||||
meant to be memory-safe and return memory-safe objects.
|
||||
"""
|
||||
def __init__(self, db_path, logid=None, eventlogging=True, debug=True):
|
||||
@@ -151,6 +153,14 @@ class Account(object):
|
||||
self.check_is_configured()
|
||||
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
|
||||
def get_latest_backupfile(self, backupdir):
|
||||
""" return the latest backup file in a given directory.
|
||||
"""
|
||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||
if res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_blobdir(self):
|
||||
""" return the directory for files.
|
||||
|
||||
@@ -160,9 +170,9 @@ class Account(object):
|
||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||
|
||||
def get_self_contact(self):
|
||||
""" return this account's identity as a :class:`deltachat.chatting.Contact`.
|
||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||
|
||||
:returns: :class:`deltachat.chatting.Contact`
|
||||
:returns: :class:`deltachat.contact.Contact`
|
||||
"""
|
||||
self.check_is_configured()
|
||||
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
||||
@@ -174,7 +184,7 @@ class Account(object):
|
||||
|
||||
:param email: email-address (text type)
|
||||
:param name: display name for this contact (optional)
|
||||
:returns: :class:`deltachat.chatting.Contact` instance.
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
name = as_dc_charpointer(name)
|
||||
email = as_dc_charpointer(email)
|
||||
@@ -200,7 +210,7 @@ class Account(object):
|
||||
whose name or e-mail matches query.
|
||||
:param only_verified: if true only return verified contacts.
|
||||
:param with_self: if true the self-contact is also returned.
|
||||
:returns: list of :class:`deltachat.chatting.Contact` objects.
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
"""
|
||||
flags = 0
|
||||
query = as_dc_charpointer(query)
|
||||
@@ -218,7 +228,7 @@ class Account(object):
|
||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
:param contact: chat_id (int) or contact object.
|
||||
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
if hasattr(contact, "id"):
|
||||
if contact._dc_context != self._dc_context:
|
||||
@@ -235,7 +245,7 @@ class Account(object):
|
||||
the specified message.
|
||||
|
||||
:param message: messsage id or message instance.
|
||||
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
if hasattr(message, "id"):
|
||||
if self._dc_context != message._dc_context:
|
||||
@@ -253,7 +263,7 @@ class Account(object):
|
||||
Chats are unpromoted until the first message is sent.
|
||||
|
||||
:param verified: if true only verified contacts can be added.
|
||||
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||
:returns: a :class:`deltachat.chat.Chat` object.
|
||||
"""
|
||||
bytes_name = name.encode("utf8")
|
||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
||||
@@ -262,7 +272,7 @@ class Account(object):
|
||||
def get_chats(self):
|
||||
""" return list of chats.
|
||||
|
||||
:returns: a list of :class:`deltachat.chatting.Chat` objects.
|
||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||
"""
|
||||
dc_chatlist = ffi.gc(
|
||||
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
|
||||
@@ -280,9 +290,24 @@ class Account(object):
|
||||
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
||||
|
||||
def get_message_by_id(self, msg_id):
|
||||
""" return Message instance. """
|
||||
""" return Message instance.
|
||||
:param msg_id: integer id of this message.
|
||||
:returns: :class:`deltachat.message.Message` instance.
|
||||
"""
|
||||
return Message.from_db(self, msg_id)
|
||||
|
||||
def get_chat_by_id(self, chat_id):
|
||||
""" return Chat instance.
|
||||
:param chat_id: integer id of this chat.
|
||||
:returns: :class:`deltachat.chat.Chat` instance.
|
||||
:raises: ValueError if chat does not exist.
|
||||
"""
|
||||
res = lib.dc_get_chat(self._dc_context, chat_id)
|
||||
if res == ffi.NULL:
|
||||
raise ValueError("cannot get chat with id={}".format(chat_id))
|
||||
lib.dc_chat_unref(res)
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def mark_seen_messages(self, messages):
|
||||
""" mark the given set of messages as seen.
|
||||
|
||||
@@ -299,7 +324,7 @@ class Account(object):
|
||||
""" Forward list of messages to a chat.
|
||||
|
||||
:param messages: list of :class:`deltachat.message.Message` object.
|
||||
:param chat: :class:`deltachat.chatting.Chat` object.
|
||||
:param chat: :class:`deltachat.chat.Chat` object.
|
||||
:returns: None
|
||||
"""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
@@ -411,7 +436,7 @@ class Account(object):
|
||||
""" setup contact and return a Chat after contact is established.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
is returned.
|
||||
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
@@ -425,7 +450,7 @@ class Account(object):
|
||||
""" join a chat group through a QR code.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
is returned which is the chat that we just joined.
|
||||
|
||||
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
|
||||
@@ -460,16 +485,20 @@ class Account(object):
|
||||
|
||||
def stop_threads(self, wait=True):
|
||||
""" stop IMAP/SMTP threads. """
|
||||
self.stop_ongoing()
|
||||
self._threads.stop(wait=wait)
|
||||
if self._threads.is_started():
|
||||
self.stop_ongoing()
|
||||
self._threads.stop(wait=wait)
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
print("SHUTDOWN", self)
|
||||
""" stop threads and close and remove underlying dc_context and callbacks. """
|
||||
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
||||
# print("SHUTDOWN", self)
|
||||
self.stop_threads(wait=False)
|
||||
print("stop_threads", self)
|
||||
self.stop_threads(wait=wait)
|
||||
print("close", self)
|
||||
lib.dc_close(self._dc_context)
|
||||
self.stop_threads(wait=wait) # to wait for threads
|
||||
print("clear", self)
|
||||
#self.stop_threads(wait=wait) # to wait for threads
|
||||
deltachat.clear_context_callback(self._dc_context)
|
||||
del self._dc_context
|
||||
atexit.unregister(self.shutdown)
|
||||
@@ -492,6 +521,20 @@ class Account(object):
|
||||
def on_dc_event_imex_file_written(self, data1, data2):
|
||||
self._imex_events.put(data1)
|
||||
|
||||
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||
"""set a new location. It effects all chats where we currently
|
||||
have enabled location streaming.
|
||||
|
||||
:param latitude: float (use 0.0 if not known)
|
||||
:param longitude: float (use 0.0 if not known)
|
||||
:param accuracy: float (use 0.0 if not known)
|
||||
:raises: ValueError if no chat is currently streaming locations
|
||||
:returns: None
|
||||
"""
|
||||
dc_res = lib.dc_set_location(self._dc_context, latitude, longitude, accuracy)
|
||||
if dc_res == 0:
|
||||
raise ValueError("no chat is streaming locations")
|
||||
|
||||
|
||||
class IOThreads:
|
||||
def __init__(self, dc_context, log_event=lambda *args: None):
|
||||
@@ -527,37 +570,45 @@ class IOThreads:
|
||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||
if wait:
|
||||
for name, thread in self._name2thread.items():
|
||||
print("joining", name)
|
||||
thread.join()
|
||||
|
||||
def imap_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_imap_jobs(self._dc_context)
|
||||
lib.dc_perform_imap_fetch(self._dc_context)
|
||||
lib.dc_perform_imap_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_imap_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
|
||||
|
||||
def mvbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
||||
|
||||
def sentbox_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
||||
|
||||
def smtp_thread_run(self):
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
||||
while not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
if not self._thread_quitflag:
|
||||
lib.dc_perform_smtp_idle(self._dc_context)
|
||||
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
||||
|
||||
|
||||
|
||||
@@ -1,58 +1,15 @@
|
||||
""" chatting related objects: Contact, Chat, Message. """
|
||||
""" Chat and Location related API. """
|
||||
|
||||
import mimetypes
|
||||
import calendar
|
||||
from datetime import datetime
|
||||
import os
|
||||
from . import props
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .message import Message
|
||||
|
||||
|
||||
class Contact(object):
|
||||
""" Delta-Chat Contact.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
def __init__(self, dc_context, id):
|
||||
self._dc_context = dc_context
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._dc_context == other._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
||||
|
||||
@property
|
||||
def _dc_contact(self):
|
||||
return ffi.gc(
|
||||
lib.dc_get_contact(self._dc_context, self.id),
|
||||
lib.dc_contact_unref
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def display_name(self):
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
|
||||
def is_verified(self):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
|
||||
class Chat(object):
|
||||
""" Chat object which manages members and through which you can send and retrieve messages.
|
||||
|
||||
@@ -312,9 +269,10 @@ class Chat(object):
|
||||
def get_contacts(self):
|
||||
""" get all contacts for this chat.
|
||||
:params: contact object.
|
||||
:returns: list of :class:`deltachat.chatting.Contact` objects for this chat
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
||||
|
||||
"""
|
||||
from .contact import Contact
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_contacts(self._dc_context, self.id),
|
||||
lib.dc_array_unref
|
||||
@@ -365,3 +323,62 @@ class Chat(object):
|
||||
if dc_res == ffi.NULL:
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
# ------ location streaming API ------------------------------
|
||||
|
||||
def is_sending_locations(self):
|
||||
"""return True if this chat has location-sending enabled currently.
|
||||
:returns: True if location sending is enabled.
|
||||
"""
|
||||
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
|
||||
|
||||
def enable_sending_locations(self, seconds):
|
||||
"""enable sending locations for this chat.
|
||||
|
||||
all subsequent messages will carry a location with them.
|
||||
"""
|
||||
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
|
||||
|
||||
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
||||
"""return list of locations for the given contact in the given timespan.
|
||||
|
||||
:param contact: the contact for which locations shall be returned.
|
||||
:param timespan_from: a datetime object or None (indicating "since beginning")
|
||||
:param timespan_to: a datetime object or None (indicating up till now)
|
||||
:returns: list of :class:`deltachat.chat.Location` objects.
|
||||
"""
|
||||
if timestamp_from is None:
|
||||
time_from = 0
|
||||
else:
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple())
|
||||
if timestamp_to is None:
|
||||
time_to = 0
|
||||
else:
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple())
|
||||
|
||||
if contact is None:
|
||||
contact_id = 0
|
||||
else:
|
||||
contact_id = contact.id
|
||||
|
||||
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
|
||||
return [
|
||||
Location(
|
||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
||||
timestamp=datetime.utcfromtimestamp(lib.dc_array_get_timestamp(dc_array, i)))
|
||||
for i in range(lib.dc_array_get_cnt(dc_array))
|
||||
]
|
||||
|
||||
|
||||
class Location:
|
||||
def __init__(self, latitude, longitude, accuracy, timestamp):
|
||||
assert isinstance(timestamp, datetime)
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.accuracy = accuracy
|
||||
self.timestamp = timestamp
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
@@ -47,7 +47,8 @@ DC_STATE_OUT_FAILED = 24
|
||||
DC_STATE_OUT_DELIVERED = 26
|
||||
DC_STATE_OUT_MDN_RCVD = 28
|
||||
DC_CONTACT_ID_SELF = 1
|
||||
DC_CONTACT_ID_DEVICE = 2
|
||||
DC_CONTACT_ID_INFO = 2
|
||||
DC_CONTACT_ID_DEVICE = 5
|
||||
DC_CONTACT_ID_LAST_SPECIAL = 9
|
||||
DC_MSG_TEXT = 10
|
||||
DC_MSG_IMAGE = 20
|
||||
|
||||
49
python/src/deltachat/contact.py
Normal file
49
python/src/deltachat/contact.py
Normal file
@@ -0,0 +1,49 @@
|
||||
""" Contact object. """
|
||||
|
||||
from . import props
|
||||
from .cutil import from_dc_charpointer
|
||||
from .capi import lib, ffi
|
||||
|
||||
|
||||
class Contact(object):
|
||||
""" Delta-Chat Contact.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||
"""
|
||||
def __init__(self, dc_context, id):
|
||||
self._dc_context = dc_context
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._dc_context == other._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
||||
|
||||
@property
|
||||
def _dc_contact(self):
|
||||
return ffi.gc(
|
||||
lib.dc_get_contact(self._dc_context, self.id),
|
||||
lib.dc_contact_unref
|
||||
)
|
||||
|
||||
@props.with_doc
|
||||
def addr(self):
|
||||
""" normalized e-mail address for this account. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||
|
||||
@props.with_doc
|
||||
def display_name(self):
|
||||
""" display name for this contact. """
|
||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||
|
||||
def is_blocked(self):
|
||||
""" Return True if the contact is blocked. """
|
||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||
|
||||
def is_verified(self):
|
||||
""" Return True if the contact is verified. """
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
@@ -1,4 +1,4 @@
|
||||
""" chatting related objects: Contact, Chat, Message. """
|
||||
""" The Message object. """
|
||||
|
||||
import os
|
||||
import shutil
|
||||
@@ -13,7 +13,7 @@ class Message(object):
|
||||
""" Message object.
|
||||
|
||||
You obtain instances of it through :class:`deltachat.account.Account` or
|
||||
:class:`deltachat.chatting.Chat`.
|
||||
:class:`deltachat.chat.Chat`.
|
||||
"""
|
||||
def __init__(self, account, dc_msg):
|
||||
self.account = account
|
||||
@@ -169,18 +169,18 @@ class Message(object):
|
||||
def chat(self):
|
||||
"""chat this message was posted in.
|
||||
|
||||
:returns: :class:`deltachat.chatting.Chat` object
|
||||
:returns: :class:`deltachat.chat.Chat` object
|
||||
"""
|
||||
from .chatting import Chat
|
||||
from .chat import Chat
|
||||
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||
return Chat(self.account, chat_id)
|
||||
|
||||
def get_sender_contact(self):
|
||||
"""return the contact of who wrote the message.
|
||||
|
||||
:returns: :class:`deltachat.chatting.Contact` instance
|
||||
:returns: :class:`deltachat.chat.Contact` instance
|
||||
"""
|
||||
from .chatting import Contact
|
||||
from .contact import Contact
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return Contact(self._dc_context, contact_id)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import print_function
|
||||
import pytest
|
||||
import os
|
||||
import queue
|
||||
import time
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from datetime import datetime, timedelta
|
||||
@@ -121,6 +122,12 @@ class TestOfflineChat:
|
||||
str(chat1)
|
||||
repr(chat1)
|
||||
|
||||
def test_chat_by_id(self, chat1):
|
||||
chat2 = chat1.account.get_chat_by_id(chat1.id)
|
||||
assert chat2 == chat1
|
||||
with pytest.raises(ValueError):
|
||||
chat1.account.get_chat_by_id(123123)
|
||||
|
||||
def test_chat_idempotent(self, chat1, ac1):
|
||||
contact1 = chat1.get_contacts()[0]
|
||||
chat2 = ac1.create_chat_by_contact(contact1.id)
|
||||
@@ -635,18 +642,29 @@ class TestOnlineAccount:
|
||||
assert os.path.exists(msg_in.filename)
|
||||
assert os.stat(msg_in.filename).st_size == os.stat(path).st_size
|
||||
|
||||
def test_import_export_online_all(self, acfactory, tmpdir):
|
||||
def test_import_export_online_all_twice(self, acfactory, tmpdir, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||
chat = ac1.create_chat_by_contact(contact1)
|
||||
chat.send_text("msg1")
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
|
||||
lp.sec("export all to {}".format(backupdir))
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path)
|
||||
t = time.time()
|
||||
|
||||
lp.sec("get fresh empty account")
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
lp.sec("get latest backup file")
|
||||
path2 = ac2.get_latest_backupfile(backupdir.strpath)
|
||||
assert path2 == path
|
||||
|
||||
lp.sec("import backup and check it's proper")
|
||||
ac2.import_all(path)
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
@@ -657,6 +675,17 @@ class TestOnlineAccount:
|
||||
assert len(messages) == 1
|
||||
assert messages[0].text == "msg1"
|
||||
|
||||
# wait until a second passed since last backup
|
||||
# because get_latest_backupfile() shall return the latest backup
|
||||
# from a UI it's unlikely anyone manages to export two
|
||||
# backups in one second.
|
||||
time.sleep(max(0, 1 - (time.time() - t)))
|
||||
lp.sec("Second-time export all to {}".format(backupdir))
|
||||
path2 = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path2)
|
||||
assert path2 != path
|
||||
assert ac2.get_latest_backupfile(backupdir.strpath) == path2
|
||||
|
||||
def test_ac_setup_message(self, acfactory, lp):
|
||||
# note that the receiving account needs to be configured and running
|
||||
# before ther setup message is send. DC does not read old messages
|
||||
@@ -810,6 +839,54 @@ class TestOnlineAccount:
|
||||
assert chat1b.get_profile_image() is None
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_send_receive_locations(self, acfactory, lp):
|
||||
now = datetime.utcnow()
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = self.get_chat(ac1, ac2)
|
||||
chat2 = self.get_chat(ac2, ac1)
|
||||
|
||||
assert not chat1.is_sending_locations()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_location(latitude=0.0, longitude=10.0)
|
||||
|
||||
ac1._evlogger.consume_events()
|
||||
ac2._evlogger.consume_events()
|
||||
|
||||
lp.sec("ac1: enable location sending in chat")
|
||||
chat1.enable_sending_locations(seconds=100)
|
||||
assert chat1.is_sending_locations()
|
||||
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
ac1.set_location(latitude=2.0, longitude=3.0, accuracy=0.5)
|
||||
ac1._evlogger.get_matching("DC_EVENT_LOCATION_CHANGED")
|
||||
chat1.send_text("hello")
|
||||
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
lp.sec("ac2: wait for incoming location message")
|
||||
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG") # "enabled-location streaming"
|
||||
|
||||
# currently core emits location changed before event_incoming message
|
||||
ac2._evlogger.get_matching("DC_EVENT_LOCATION_CHANGED")
|
||||
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG") # text message with location
|
||||
|
||||
locations = chat2.get_locations()
|
||||
assert len(locations) == 1
|
||||
assert locations[0].latitude == 2.0
|
||||
assert locations[0].longitude == 3.0
|
||||
assert locations[0].accuracy == 0.5
|
||||
assert locations[0].timestamp > now
|
||||
|
||||
contact = ac2.create_contact(ac1.get_config("addr"))
|
||||
locations2 = chat2.get_locations(contact=contact)
|
||||
assert len(locations2) == 1
|
||||
assert locations2 == locations
|
||||
|
||||
contact = ac2.create_contact("nonexisting@example.org")
|
||||
locations3 = chat2.get_locations(contact=contact)
|
||||
assert not locations3
|
||||
|
||||
|
||||
class TestOnlineConfigureFails:
|
||||
def test_invalid_password(self, acfactory):
|
||||
|
||||
104
src/blob.rs
104
src/blob.rs
@@ -1,3 +1,4 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
@@ -24,7 +25,8 @@ impl<'a> BlobObject<'a> {
|
||||
/// Creates a new file in the blob directory. The name will be
|
||||
/// derived from the platform-agnostic basename of the suggested
|
||||
/// name, followed by a random number and followed by a possible
|
||||
/// extension. The `data` will be written into the file.
|
||||
/// extension. The `data` will be written into the file without
|
||||
/// race-conditions.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -41,29 +43,33 @@ impl<'a> BlobObject<'a> {
|
||||
data: &[u8],
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let blobdir = context.get_blobdir();
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref().to_string());
|
||||
let mut name = format!("{}{}", stem, ext);
|
||||
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
|
||||
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
|
||||
file.write_all(data)
|
||||
.map_err(|err| BlobError::new_write_failure(blobdir, &name, err))?;
|
||||
let blob = BlobObject {
|
||||
blobdir,
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
// Creates a new file, returning a tuple of the name and the handle.
|
||||
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> {
|
||||
let max_attempt = 15;
|
||||
let mut name = format!("{}{}", stem, ext);
|
||||
for attempt in 0..max_attempt {
|
||||
let path = blobdir.join(&name);
|
||||
let path = dir.join(&name);
|
||||
match fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
file.write_all(data)
|
||||
.map_err(|err| BlobError::new_write_failure(blobdir, &name, err))?;
|
||||
let blob = BlobObject {
|
||||
blobdir,
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
return Ok(blob);
|
||||
}
|
||||
Ok(file) => return Ok((name, file)),
|
||||
Err(err) => {
|
||||
if attempt == max_attempt {
|
||||
return Err(BlobError::new_create_failure(blobdir, &name, err));
|
||||
return Err(BlobError::new_create_failure(dir, &name, err));
|
||||
} else {
|
||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
||||
}
|
||||
@@ -71,7 +77,7 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
}
|
||||
Err(BlobError::new_create_failure(
|
||||
blobdir,
|
||||
dir,
|
||||
&name,
|
||||
format_err!("Unreachable code - supposedly"),
|
||||
))
|
||||
@@ -80,7 +86,9 @@ impl<'a> BlobObject<'a> {
|
||||
/// Creates a new blob object with unique name by copying an existing file.
|
||||
///
|
||||
/// This creates a new blob as described in [BlobObject::create]
|
||||
/// but also copies an existing file into it.
|
||||
/// but also copies an existing file into it. This is done in a
|
||||
/// in way which avoids race-conditions when multiple files are
|
||||
/// concurrently created.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -91,11 +99,24 @@ impl<'a> BlobObject<'a> {
|
||||
context: &'a Context,
|
||||
src: impl AsRef<Path>,
|
||||
) -> std::result::Result<BlobObject<'a>, BlobError> {
|
||||
let blob = BlobObject::create(context, src.as_ref().to_string_lossy(), b"")?;
|
||||
fs::copy(src.as_ref(), blob.to_abs_path()).map_err(|err| {
|
||||
fs::remove_file(blob.to_abs_path()).ok();
|
||||
BlobError::new_copy_failure(blob.blobdir, &blob.name, src.as_ref(), err)
|
||||
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| {
|
||||
BlobError::new_copy_failure(context.get_blobdir(), "", src.as_ref(), err)
|
||||
})?;
|
||||
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
|
||||
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
|
||||
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
|
||||
{
|
||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||
let path = context.get_blobdir().join(&name);
|
||||
fs::remove_file(path).ok();
|
||||
}
|
||||
BlobError::new_copy_failure(context.get_blobdir(), &name, src.as_ref(), err)
|
||||
})?;
|
||||
let blob = BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
name: format!("$BLOBDIR/{}", name),
|
||||
};
|
||||
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
@@ -145,6 +166,9 @@ impl<'a> BlobObject<'a> {
|
||||
.as_ref()
|
||||
.strip_prefix(context.get_blobdir())
|
||||
.map_err(|_| BlobError::new_wrong_blobdir(context.get_blobdir(), path.as_ref()))?;
|
||||
if !BlobObject::is_acceptible_blob_name(&rel_path) {
|
||||
return Err(BlobError::new_wrong_name(path.as_ref()));
|
||||
}
|
||||
let name = rel_path
|
||||
.to_str()
|
||||
.ok_or_else(|| BlobError::new_wrong_name(path.as_ref()))?;
|
||||
@@ -171,8 +195,7 @@ impl<'a> BlobObject<'a> {
|
||||
true => name.splitn(2, '/').last().unwrap().to_string(),
|
||||
false => name,
|
||||
};
|
||||
let (stem, ext) = BlobObject::sanitise_name(name.clone());
|
||||
if format!("{}{}", stem, ext) != name.as_ref() {
|
||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||
return Err(BlobError::new_wrong_name(name));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
@@ -236,7 +259,8 @@ impl<'a> BlobObject<'a> {
|
||||
/// ".txt")` while "bar" is returned as `("bar", "")`.
|
||||
///
|
||||
/// The extension part will always be lowercased.
|
||||
fn sanitise_name(mut name: String) -> (String, String) {
|
||||
fn sanitise_name(name: &str) -> (String, String) {
|
||||
let mut name = name.to_string();
|
||||
for part in name.rsplit('/') {
|
||||
if part.len() > 0 {
|
||||
name = part.to_string();
|
||||
@@ -266,6 +290,28 @@ impl<'a> BlobObject<'a> {
|
||||
_ => (stem, format!(".{}", ext).to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether a name is a valid blob name.
|
||||
///
|
||||
/// This is slightly less strict than stanitise_name, presumably
|
||||
/// someone already created a file with such a name so we just
|
||||
/// ensure it's not actually a path in disguise is actually utf-8.
|
||||
fn is_acceptible_blob_name(name: impl AsRef<OsStr>) -> bool {
|
||||
let uname = match name.as_ref().to_str() {
|
||||
Some(name) => name,
|
||||
None => return false,
|
||||
};
|
||||
if uname.find('/').is_some() {
|
||||
return false;
|
||||
}
|
||||
if uname.find('\\').is_some() {
|
||||
return false;
|
||||
}
|
||||
if uname.find('\0').is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for BlobObject<'a> {
|
||||
@@ -615,4 +661,14 @@ mod tests {
|
||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_blob_name() {
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||
}
|
||||
}
|
||||
|
||||
89
src/chat.rs
89
src/chat.rs
@@ -97,6 +97,8 @@ impl Chat {
|
||||
|
||||
if chat.param.exists(Param::Selftalk) {
|
||||
chat.name = context.stock_str(StockMessage::SelfMsg).into();
|
||||
} else if chat.param.exists(Param::Devicetalk) {
|
||||
chat.name = context.stock_str(StockMessage::DeviceMessages).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +111,14 @@ impl Chat {
|
||||
self.param.exists(Param::Selftalk)
|
||||
}
|
||||
|
||||
pub fn is_device_talk(&self) -> bool {
|
||||
self.param.exists(Param::Devicetalk)
|
||||
}
|
||||
|
||||
pub fn can_send(&self) -> bool {
|
||||
self.id > DC_CHAT_ID_LAST_SPECIAL && !self.is_device_talk()
|
||||
}
|
||||
|
||||
pub fn update_param(&mut self, context: &Context) -> Result<(), Error> {
|
||||
sql::execute(
|
||||
context,
|
||||
@@ -258,7 +268,7 @@ impl Chat {
|
||||
}
|
||||
|
||||
if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
|
||||
&& !is_contact_in_chat(context, self.id, 1 as u32)
|
||||
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF)
|
||||
{
|
||||
emit_event!(
|
||||
context,
|
||||
@@ -414,7 +424,7 @@ impl Chat {
|
||||
&context.sql,
|
||||
"INSERT INTO locations \
|
||||
(timestamp,from_id,chat_id, latitude,longitude,independent)\
|
||||
VALUES (?,?,?, ?,?,1);",
|
||||
VALUES (?,?,?, ?,?,1);", // 1=DC_CONTACT_ID_SELF
|
||||
params![
|
||||
timestamp,
|
||||
DC_CONTACT_ID_SELF,
|
||||
@@ -446,7 +456,7 @@ impl Chat {
|
||||
params![
|
||||
new_rfc724_mid,
|
||||
self.id as i32,
|
||||
1i32,
|
||||
DC_CONTACT_ID_SELF,
|
||||
to_id as i32,
|
||||
timestamp,
|
||||
msg.type_0,
|
||||
@@ -582,6 +592,12 @@ pub fn set_blocking(context: &Context, chat_id: u32, new_blocking: Blocked) -> b
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn copy_device_icon_to_blobs(context: &Context) -> Result<String, Error> {
|
||||
let icon = include_bytes!("../assets/icon-device.png");
|
||||
let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?;
|
||||
Ok(blob.as_name().to_string())
|
||||
}
|
||||
|
||||
pub fn create_or_lookup_by_contact_id(
|
||||
context: &Context,
|
||||
contact_id: u32,
|
||||
@@ -605,7 +621,14 @@ pub fn create_or_lookup_by_contact_id(
|
||||
"INSERT INTO chats (type, name, param, blocked, grpid) VALUES({}, '{}', '{}', {}, '{}')",
|
||||
100,
|
||||
chat_name,
|
||||
if contact_id == DC_CONTACT_ID_SELF as u32 { "K=1" } else { "" },
|
||||
match contact_id {
|
||||
DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk
|
||||
DC_CONTACT_ID_DEVICE => {
|
||||
let icon = copy_device_icon_to_blobs(context)?;
|
||||
format!("D=1\ni={}", icon) // D = Param::Devicetalk, i = Param::ProfileImage
|
||||
},
|
||||
_ => "".to_string()
|
||||
},
|
||||
create_blocked as u8,
|
||||
contact.get_addr(),
|
||||
),
|
||||
@@ -677,8 +700,7 @@ pub fn msgtype_has_file(msgtype: Viewtype) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Result<MsgId, Error> {
|
||||
msg.id = MsgId::new_unset();
|
||||
fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
|
||||
if msg.type_0 == Viewtype::Text {
|
||||
// the caller should check if the message text is empty
|
||||
} else if msgtype_has_file(msg.type_0) {
|
||||
@@ -714,10 +736,16 @@ fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Res
|
||||
} else {
|
||||
bail!("Cannot send messages of type #{}.", msg.type_0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Result<MsgId, Error> {
|
||||
msg.id = MsgId::new_unset();
|
||||
prepare_msg_blob(context, msg)?;
|
||||
unarchive(context, chat_id)?;
|
||||
|
||||
let mut chat = Chat::load_from_db(context, chat_id)?;
|
||||
ensure!(chat.can_send(), "cannot send to chat #{}", chat_id);
|
||||
|
||||
// The OutPreparing state is set by dc_prepare_msg() before it
|
||||
// calls this function and the message is left in the OutPreparing
|
||||
@@ -914,7 +942,7 @@ fn do_set_draft(context: &Context, chat_id: u32, msg: &mut Message) -> Result<()
|
||||
VALUES (?,?,?, ?,?,?,?,?);",
|
||||
params![
|
||||
chat_id as i32,
|
||||
1,
|
||||
DC_CONTACT_ID_SELF,
|
||||
time(),
|
||||
msg.type_0,
|
||||
MessageState::OutDraft,
|
||||
@@ -993,8 +1021,8 @@ pub fn get_chat_msgs(
|
||||
" ON m.chat_id=chats.id",
|
||||
" LEFT JOIN contacts",
|
||||
" ON m.from_id=contacts.id",
|
||||
" WHERE m.from_id!=1",
|
||||
" AND m.from_id!=2",
|
||||
" WHERE m.from_id!=1", // 1=DC_CONTACT_ID_SELF
|
||||
" AND m.from_id!=2", // 2=DC_CONTACT_ID_INFO
|
||||
" AND m.hidden=0",
|
||||
" AND chats.blocked=2",
|
||||
" AND contacts.blocked=0",
|
||||
@@ -1350,7 +1378,7 @@ pub fn create_group_chat(
|
||||
let chat_id = sql::get_rowid(context, &context.sql, "chats", "grpid", grpid);
|
||||
|
||||
if chat_id != 0 {
|
||||
if add_to_chat_contacts_table(context, chat_id, 1) {
|
||||
if add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF) {
|
||||
let mut draft_msg = Message::new(Viewtype::Text);
|
||||
draft_msg.set_text(Some(draft_txt));
|
||||
set_draft_raw(context, chat_id, &mut draft_msg);
|
||||
@@ -1557,7 +1585,7 @@ pub fn remove_contact_from_chat(
|
||||
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
|
||||
if let Ok(chat) = Chat::load_from_db(context, chat_id) {
|
||||
if real_group_exists(context, chat_id) {
|
||||
if !is_contact_in_chat(context, chat_id, 1 as u32) {
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup(
|
||||
@@ -1653,7 +1681,7 @@ pub fn set_chat_name(
|
||||
if real_group_exists(context, chat_id) {
|
||||
if chat.name == new_name.as_ref() {
|
||||
success = true;
|
||||
} else if !is_contact_in_chat(context, chat_id, 1) {
|
||||
} else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
|
||||
emit_event!(
|
||||
context,
|
||||
Event::ErrorSelfNotInGroup("Cannot set chat name; self not in group".into())
|
||||
@@ -1898,15 +1926,46 @@ pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef<str>) -> (u32,
|
||||
.unwrap_or((0, false, Blocked::Not))
|
||||
}
|
||||
|
||||
pub fn add_device_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
|
||||
pub fn add_device_msg(context: &Context, msg: &mut Message) -> Result<MsgId, Error> {
|
||||
let (chat_id, _blocked) =
|
||||
create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not)?;
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
|
||||
prepare_msg_blob(context, msg)?;
|
||||
unarchive(context, chat_id)?;
|
||||
|
||||
context.sql.execute(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \
|
||||
VALUES (?,?,?, ?,?,?, ?,?,?);",
|
||||
params![
|
||||
chat_id,
|
||||
DC_CONTACT_ID_DEVICE,
|
||||
DC_CONTACT_ID_SELF,
|
||||
dc_create_smeared_timestamp(context),
|
||||
msg.type_0,
|
||||
MessageState::InFresh,
|
||||
msg.text.as_ref().map_or("", String::as_str),
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
],
|
||||
)?;
|
||||
|
||||
let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid);
|
||||
let msg_id = MsgId::new(row_id);
|
||||
context.call_cb(Event::IncomingMsg { chat_id, msg_id });
|
||||
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
|
||||
if context.sql.execute(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);",
|
||||
params![
|
||||
chat_id as i32,
|
||||
2,
|
||||
2,
|
||||
DC_CONTACT_ID_INFO,
|
||||
DC_CONTACT_ID_INFO,
|
||||
dc_create_smeared_timestamp(context),
|
||||
Viewtype::Text,
|
||||
MessageState::InNoticed,
|
||||
|
||||
@@ -295,7 +295,7 @@ impl Chatlist {
|
||||
let mut lastcontact = None;
|
||||
|
||||
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
|
||||
if lastmsg.from_id != 1
|
||||
if lastmsg.from_id != DC_CONTACT_ID_SELF
|
||||
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
|
||||
{
|
||||
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
|
||||
|
||||
@@ -62,16 +62,16 @@ pub fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &Context) {
|
||||
|
||||
let mut param_autoconfig: Option<LoginParam> = None;
|
||||
|
||||
context.inbox.read().unwrap().disconnect(context);
|
||||
context.inbox.write().unwrap().disconnect(context);
|
||||
context
|
||||
.sentbox_thread
|
||||
.read()
|
||||
.write()
|
||||
.unwrap()
|
||||
.imap
|
||||
.disconnect(context);
|
||||
context
|
||||
.mvbox_thread
|
||||
.read()
|
||||
.write()
|
||||
.unwrap()
|
||||
.imap
|
||||
.disconnect(context);
|
||||
@@ -357,7 +357,7 @@ pub fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &Context) {
|
||||
};
|
||||
context
|
||||
.inbox
|
||||
.read()
|
||||
.write()
|
||||
.unwrap()
|
||||
.configure_folders(context, flags);
|
||||
true
|
||||
@@ -398,7 +398,7 @@ pub fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &Context) {
|
||||
}
|
||||
}
|
||||
if imap_connected_here {
|
||||
context.inbox.read().unwrap().disconnect(context);
|
||||
context.inbox.write().unwrap().disconnect(context);
|
||||
}
|
||||
if smtp_connected_here {
|
||||
context.smtp.clone().lock().unwrap().disconnect();
|
||||
@@ -484,7 +484,7 @@ fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
|
||||
param.mail_user, param.mail_server, param.mail_port, param.server_flags
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
if context.inbox.read().unwrap().connect(context, ¶m) {
|
||||
if context.inbox.write().unwrap().connect(context, ¶m) {
|
||||
info!(context, "success: {}", inf);
|
||||
return Some(true);
|
||||
}
|
||||
@@ -531,27 +531,32 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
|
||||
param.send_user, param.send_server, param.send_port, param.server_flags
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
if context
|
||||
match context
|
||||
.smtp
|
||||
.clone()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.connect(context, ¶m)
|
||||
{
|
||||
info!(context, "success: {}", inf);
|
||||
return Some(true);
|
||||
Ok(()) => {
|
||||
info!(context, "success: {}", inf);
|
||||
Some(true)
|
||||
}
|
||||
Err(err) => {
|
||||
if context.shall_stop_ongoing() {
|
||||
Some(false)
|
||||
} else {
|
||||
warn!(context, "could not connect: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
if context.shall_stop_ongoing() {
|
||||
return Some(false);
|
||||
}
|
||||
info!(context, "could not connect: {}", inf);
|
||||
None
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Connect to configured account
|
||||
******************************************************************************/
|
||||
pub fn dc_connect_to_configured_imap(context: &Context, imap: &Imap) -> libc::c_int {
|
||||
pub fn dc_connect_to_configured_imap(context: &Context, imap: &mut Imap) -> libc::c_int {
|
||||
let mut ret_connected = 0;
|
||||
|
||||
if imap.is_connected() {
|
||||
|
||||
@@ -130,7 +130,8 @@ const DC_MAX_GET_INFO_LEN: usize = 100000;
|
||||
|
||||
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
|
||||
pub const DC_CONTACT_ID_SELF: u32 = 1;
|
||||
pub const DC_CONTACT_ID_DEVICE: u32 = 2;
|
||||
pub const DC_CONTACT_ID_INFO: u32 = 2;
|
||||
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
|
||||
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
pub const DC_CREATE_MVBOX: usize = 1;
|
||||
|
||||
@@ -153,7 +153,16 @@ impl Contact {
|
||||
blocked: false,
|
||||
origin: Origin::Unknown,
|
||||
};
|
||||
|
||||
return Ok(contact);
|
||||
} else if contact_id == DC_CONTACT_ID_DEVICE {
|
||||
let contact = Contact {
|
||||
id: contact_id,
|
||||
name: context.stock_str(StockMessage::DeviceMessages).into(),
|
||||
authname: "".into(),
|
||||
addr: "device@localhost".into(),
|
||||
blocked: false,
|
||||
origin: Origin::Unknown,
|
||||
};
|
||||
return Ok(contact);
|
||||
}
|
||||
|
||||
@@ -264,7 +273,7 @@ impl Contact {
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_normalized == addr_self {
|
||||
return 1;
|
||||
return DC_CONTACT_ID_SELF;
|
||||
}
|
||||
|
||||
context.sql.query_get_value(
|
||||
@@ -301,7 +310,7 @@ impl Contact {
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr == addr_self {
|
||||
return Ok((1, sth_modified));
|
||||
return Ok((DC_CONTACT_ID_SELF, sth_modified));
|
||||
}
|
||||
|
||||
if !may_be_valid_addr(&addr) {
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct Context {
|
||||
blobdir: PathBuf,
|
||||
pub sql: Sql,
|
||||
pub inbox: Arc<RwLock<Imap>>,
|
||||
pub(crate) inbox_watch: Arc<(Mutex<bool>, Condvar)>,
|
||||
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
|
||||
pub probe_imap_network: Arc<RwLock<bool>>,
|
||||
pub sentbox_thread: Arc<RwLock<JobThread>>,
|
||||
@@ -115,10 +116,15 @@ impl Context {
|
||||
"Blobdir does not exist: {}",
|
||||
blobdir.display()
|
||||
);
|
||||
|
||||
let inbox_watch = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let inbox = Arc::new(RwLock::new(Imap::new(inbox_watch.clone())));
|
||||
|
||||
let ctx = Context {
|
||||
blobdir,
|
||||
dbfile,
|
||||
inbox: Arc::new(RwLock::new(Imap::new())),
|
||||
inbox,
|
||||
inbox_watch,
|
||||
cb,
|
||||
os_name: Some(os_name),
|
||||
running_state: Arc::new(RwLock::new(Default::default())),
|
||||
@@ -132,12 +138,10 @@ impl Context {
|
||||
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
|
||||
"SENTBOX",
|
||||
"configured_sentbox_folder",
|
||||
Imap::new(),
|
||||
))),
|
||||
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
|
||||
"MVBOX",
|
||||
"configured_mvbox_folder",
|
||||
Imap::new(),
|
||||
))),
|
||||
probe_imap_network: Arc::new(RwLock::new(false)),
|
||||
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
|
||||
@@ -454,19 +458,28 @@ impl Context {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_inbox_idle(&self) {
|
||||
let &(ref lock, ref cvar) = &*self.inbox_watch.clone();
|
||||
let mut watch = lock.lock().unwrap();
|
||||
|
||||
*watch = true;
|
||||
cvar.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
info!(self, "disconnecting INBOX-watch",);
|
||||
self.inbox.read().unwrap().disconnect(self);
|
||||
self.inbox.write().unwrap().disconnect(self);
|
||||
info!(self, "disconnecting sentbox-thread",);
|
||||
self.sentbox_thread.read().unwrap().imap.disconnect(self);
|
||||
self.sentbox_thread.write().unwrap().imap.disconnect(self);
|
||||
info!(self, "disconnecting mvbox-thread",);
|
||||
self.mvbox_thread.read().unwrap().imap.disconnect(self);
|
||||
self.mvbox_thread.write().unwrap().imap.disconnect(self);
|
||||
info!(self, "disconnecting SMTP");
|
||||
self.smtp.clone().lock().unwrap().disconnect();
|
||||
self.sql.close(self);
|
||||
info!(self, "Context closed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,18 +117,30 @@ impl<'a> MimeParser<'a> {
|
||||
);
|
||||
|
||||
if r == MAILIMF_NO_ERROR as libc::c_int && !self.mimeroot.is_null() {
|
||||
let (encrypted, signatures, gossipped_addr) =
|
||||
e2ee::try_decrypt(self.context, self.mimeroot)?;
|
||||
self.encrypted = encrypted;
|
||||
self.signatures = signatures;
|
||||
self.gossipped_addr = gossipped_addr;
|
||||
match e2ee::try_decrypt(self.context, self.mimeroot) {
|
||||
Ok((encrypted, signatures, gossipped_addr)) => {
|
||||
self.encrypted = encrypted;
|
||||
self.signatures = signatures;
|
||||
self.gossipped_addr = gossipped_addr;
|
||||
}
|
||||
Err(err) => {
|
||||
// continue with the current, still encrypted, mime tree.
|
||||
// unencrypted parts will be replaced by an error message
|
||||
// that is added as "the message" to the chat then.
|
||||
//
|
||||
// if we just return here, the header is missing
|
||||
// and the caller cannot display the message
|
||||
// and try to assign the message to a chat
|
||||
warn!(self.context, "decryption failed: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
self.parse_mime_recursive(self.mimeroot);
|
||||
|
||||
if let Some(field) = self.lookup_field("Subject") {
|
||||
if (*field).fld_type == MAILIMF_FIELD_SUBJECT as libc::c_int {
|
||||
let subj = (*(*field).fld_data.fld_subject).sbj_value;
|
||||
|
||||
self.subject = as_opt_str(subj).map(dc_decode_header_words);
|
||||
self.subject = to_opt_string_lossy(subj).map(|x| dc_decode_header_words(&x));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +611,7 @@ impl<'a> MimeParser<'a> {
|
||||
let mut decoded_data = match wrapmime::mailmime_transfer_decode(mime) {
|
||||
Ok(decoded_data) => decoded_data,
|
||||
Err(_) => {
|
||||
// Note that it's now always an error - might be no data
|
||||
// Note that it's not always an error - might be no data
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -630,7 +642,7 @@ impl<'a> MimeParser<'a> {
|
||||
self.context,
|
||||
"Cannot convert {} bytes from \"{}\" to \"utf-8\".",
|
||||
decoded_data.len(),
|
||||
as_str(charset),
|
||||
to_string_lossy(charset),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -729,33 +741,21 @@ impl<'a> MimeParser<'a> {
|
||||
if !(*mime).mm_content_type.is_null()
|
||||
&& !(*(*mime).mm_content_type).ct_subtype.is_null()
|
||||
{
|
||||
desired_filename =
|
||||
format!("file.{}", as_str((*(*mime).mm_content_type).ct_subtype));
|
||||
desired_filename = format!(
|
||||
"file.{}",
|
||||
to_string_lossy((*(*mime).mm_content_type).ct_subtype)
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if desired_filename.starts_with("location") && desired_filename.ends_with(".kml") {
|
||||
if !decoded_data.is_empty() {
|
||||
let d = std::string::String::from_utf8_lossy(&decoded_data);
|
||||
self.location_kml = location::Kml::parse(self.context, &d).ok();
|
||||
}
|
||||
} else if desired_filename.starts_with("message")
|
||||
&& desired_filename.ends_with(".kml")
|
||||
{
|
||||
if !decoded_data.is_empty() {
|
||||
let d = std::string::String::from_utf8_lossy(&decoded_data);
|
||||
self.message_kml = location::Kml::parse(self.context, &d).ok();
|
||||
}
|
||||
} else if !decoded_data.is_empty() {
|
||||
self.do_add_single_file_part(
|
||||
msg_type,
|
||||
mime_type,
|
||||
raw_mime.as_ref(),
|
||||
&decoded_data,
|
||||
&desired_filename,
|
||||
);
|
||||
}
|
||||
self.do_add_single_file_part(
|
||||
msg_type,
|
||||
mime_type,
|
||||
raw_mime.as_ref(),
|
||||
&decoded_data,
|
||||
&desired_filename,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -769,20 +769,44 @@ impl<'a> MimeParser<'a> {
|
||||
mime_type: libc::c_int,
|
||||
raw_mime: Option<&String>,
|
||||
decoded_data: &[u8],
|
||||
desired_filename: &str,
|
||||
filename: &str,
|
||||
) {
|
||||
/* write decoded data to new blob file */
|
||||
let blob = match BlobObject::create(self.context, desired_filename, decoded_data) {
|
||||
if decoded_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
// treat location/message kml file attachments specially
|
||||
if filename.ends_with(".kml") {
|
||||
// XXX what if somebody sends eg an "location-highlights.kml"
|
||||
// attachment unrelated to location streaming?
|
||||
if filename.starts_with("location") || filename.starts_with("message") {
|
||||
let parsed = location::Kml::parse(self.context, decoded_data)
|
||||
.map_err(|err| {
|
||||
warn!(self.context, "failed to parse kml part: {}", err);
|
||||
})
|
||||
.ok();
|
||||
if filename.starts_with("location") {
|
||||
self.location_kml = parsed;
|
||||
} else {
|
||||
self.message_kml = parsed;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* we have a regular file attachment,
|
||||
write decoded data to new blob object */
|
||||
|
||||
let blob = match BlobObject::create(self.context, filename, decoded_data) {
|
||||
Ok(blob) => blob,
|
||||
Err(err) => {
|
||||
error!(
|
||||
self.context,
|
||||
"Could not add blob for mime part {}, error {}", desired_filename, err
|
||||
"Could not add blob for mime part {}, error {}", filename, err
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/* create and register Mime part referencing the new Blob object */
|
||||
let mut part = Part::default();
|
||||
part.typ = msg_type;
|
||||
part.mimetype = mime_type;
|
||||
@@ -802,10 +826,15 @@ impl<'a> MimeParser<'a> {
|
||||
}
|
||||
|
||||
fn do_add_single_part(&mut self, mut part: Part) {
|
||||
if self.encrypted && self.signatures.len() > 0 {
|
||||
part.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
} else if self.encrypted {
|
||||
part.param.set_int(Param::ErroneousE2ee, 0x2);
|
||||
if self.encrypted {
|
||||
if self.signatures.len() > 0 {
|
||||
part.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
} else {
|
||||
// XXX if the message was encrypted but not signed
|
||||
// it's not neccessarily an error we need to signal.
|
||||
// we could just treat it as if it was not encrypted.
|
||||
part.param.set_int(Param::ErroneousE2ee, 0x2);
|
||||
}
|
||||
}
|
||||
self.parts.push(part);
|
||||
}
|
||||
@@ -850,7 +879,8 @@ impl<'a> MimeParser<'a> {
|
||||
}) as *mut mailimf_mailbox;
|
||||
|
||||
if !mb.is_null() {
|
||||
let from_addr_norm = addr_normalize(as_str((*mb).mb_addr_spec));
|
||||
let from_addr = to_string_lossy((*mb).mb_addr_spec);
|
||||
let from_addr_norm = addr_normalize(&from_addr);
|
||||
let recipients = wrapmime::mailimf_get_recipients(self.header_root);
|
||||
if recipients.len() == 1 && recipients.contains(from_addr_norm) {
|
||||
sender_equals_recipient = true;
|
||||
@@ -989,15 +1019,16 @@ unsafe fn mailmime_get_mime_type(mime: *mut Mailmime) -> (libc::c_int, Viewtype,
|
||||
}
|
||||
}
|
||||
|
||||
let raw_mime = reconcat_mime(Some("text"), as_opt_str((*c).ct_subtype));
|
||||
let raw_mime = reconcat_mime(Some("text"), to_opt_string_lossy((*c).ct_subtype));
|
||||
(DC_MIMETYPE_FILE, Viewtype::File, Some(raw_mime))
|
||||
}
|
||||
MAILMIME_DISCRETE_TYPE_IMAGE => {
|
||||
let subtype = as_opt_str((*c).ct_subtype);
|
||||
let msg_type = match subtype {
|
||||
let subtype = to_opt_string_lossy((*c).ct_subtype);
|
||||
let msg_type = match subtype.as_ref().map(|x| x.as_str()) {
|
||||
Some("gif") => Viewtype::Gif,
|
||||
Some("svg+xml") => {
|
||||
let raw_mime = reconcat_mime(Some("image"), as_opt_str((*c).ct_subtype));
|
||||
let raw_mime =
|
||||
reconcat_mime(Some("image"), to_opt_string_lossy((*c).ct_subtype));
|
||||
return (DC_MIMETYPE_FILE, Viewtype::File, Some(raw_mime));
|
||||
}
|
||||
_ => Viewtype::Image,
|
||||
@@ -1007,11 +1038,11 @@ unsafe fn mailmime_get_mime_type(mime: *mut Mailmime) -> (libc::c_int, Viewtype,
|
||||
(DC_MIMETYPE_IMAGE, msg_type, Some(raw_mime))
|
||||
}
|
||||
MAILMIME_DISCRETE_TYPE_AUDIO => {
|
||||
let raw_mime = reconcat_mime(Some("audio"), as_opt_str((*c).ct_subtype));
|
||||
let raw_mime = reconcat_mime(Some("audio"), to_opt_string_lossy((*c).ct_subtype));
|
||||
(DC_MIMETYPE_AUDIO, Viewtype::Audio, Some(raw_mime))
|
||||
}
|
||||
MAILMIME_DISCRETE_TYPE_VIDEO => {
|
||||
let raw_mime = reconcat_mime(Some("video"), as_opt_str((*c).ct_subtype));
|
||||
let raw_mime = reconcat_mime(Some("video"), to_opt_string_lossy((*c).ct_subtype));
|
||||
(DC_MIMETYPE_VIDEO, Viewtype::Video, Some(raw_mime))
|
||||
}
|
||||
_ => {
|
||||
@@ -1022,13 +1053,15 @@ unsafe fn mailmime_get_mime_type(mime: *mut Mailmime) -> (libc::c_int, Viewtype,
|
||||
b"autocrypt-setup\x00" as *const u8 as *const libc::c_char,
|
||||
) == 0i32
|
||||
{
|
||||
let raw_mime = reconcat_mime(None, as_opt_str((*c).ct_subtype));
|
||||
let raw_mime = reconcat_mime(None, to_opt_string_lossy((*c).ct_subtype));
|
||||
return (DC_MIMETYPE_AC_SETUP_FILE, Viewtype::File, Some(raw_mime));
|
||||
}
|
||||
|
||||
let raw_mime = reconcat_mime(
|
||||
as_opt_str((*(*(*c).ct_type).tp_data.tp_discrete_type).dt_extension),
|
||||
as_opt_str((*c).ct_subtype),
|
||||
to_opt_string_lossy((*(*(*c).ct_type).tp_data.tp_discrete_type).dt_extension)
|
||||
.as_ref()
|
||||
.map(|x| x.as_str()),
|
||||
to_opt_string_lossy((*c).ct_subtype),
|
||||
);
|
||||
|
||||
(DC_MIMETYPE_FILE, Viewtype::File, Some(raw_mime))
|
||||
@@ -1038,9 +1071,9 @@ unsafe fn mailmime_get_mime_type(mime: *mut Mailmime) -> (libc::c_int, Viewtype,
|
||||
if (*(*(*c).ct_type).tp_data.tp_composite_type).ct_type
|
||||
== MAILMIME_COMPOSITE_TYPE_MULTIPART as libc::c_int
|
||||
{
|
||||
let subtype = as_opt_str((*c).ct_subtype);
|
||||
let subtype = to_opt_string_lossy((*c).ct_subtype);
|
||||
|
||||
let mime_type = match subtype {
|
||||
let mime_type = match subtype.as_ref().map(|x| x.as_str()) {
|
||||
Some("alternative") => DC_MIMETYPE_MP_ALTERNATIVE,
|
||||
Some("related") => DC_MIMETYPE_MP_RELATED,
|
||||
Some("encrypted") => {
|
||||
@@ -1075,9 +1108,9 @@ unsafe fn mailmime_get_mime_type(mime: *mut Mailmime) -> (libc::c_int, Viewtype,
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcat_mime(typ: Option<&str>, subtype: Option<&str>) -> String {
|
||||
fn reconcat_mime(typ: Option<&str>, subtype: Option<String>) -> String {
|
||||
let typ = typ.unwrap_or("application");
|
||||
let subtype = subtype.unwrap_or("octet-stream");
|
||||
let subtype = subtype.unwrap_or("octet-stream".to_string());
|
||||
|
||||
format!("{}/{}", typ, subtype)
|
||||
}
|
||||
@@ -1224,13 +1257,13 @@ mod tests {
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn test_dc_mailmime_parse_crash_fuzzy(data in "[!-~\t ]{2000,}") {
|
||||
// this test doesn't exercise much of dc_mimeparser anymore
|
||||
// because a missing From-field early aborts parsing
|
||||
let context = dummy_context();
|
||||
let mut mimeparser = MimeParser::new(&context.ctx);
|
||||
unsafe {
|
||||
assert!(mimeparser.parse(data.as_bytes()).is_err());
|
||||
}
|
||||
|
||||
// parsing should always succeed
|
||||
// but the returned header will normally be empty on random data
|
||||
assert!(unsafe {mimeparser.parse(data.as_bytes()).is_ok()});
|
||||
assert!(mimeparser.header.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,12 +65,12 @@ pub unsafe fn dc_receive_imf(
|
||||
|
||||
let mut mime_parser = MimeParser::new(context);
|
||||
if let Err(err) = mime_parser.parse(imf_raw) {
|
||||
error!(context, "dc_receive_imf parse error: {}", err);
|
||||
warn!(context, "dc_receive_imf parse error: {}", err);
|
||||
};
|
||||
|
||||
if mime_parser.header.is_empty() {
|
||||
// Error - even adding an empty record won't help as we do not know the message ID
|
||||
info!(context, "No header.");
|
||||
warn!(context, "No header.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ pub unsafe fn dc_receive_imf(
|
||||
&mut created_db_entries,
|
||||
&mut create_event_to_send,
|
||||
) {
|
||||
info!(context, "{}", err);
|
||||
warn!(context, "{}", err);
|
||||
|
||||
cleanup(
|
||||
context,
|
||||
@@ -257,7 +257,7 @@ pub unsafe fn dc_receive_imf(
|
||||
);
|
||||
}
|
||||
|
||||
if !mime_parser.message_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL {
|
||||
if mime_parser.location_kml.is_some() || mime_parser.message_kml.is_some() {
|
||||
save_locations(
|
||||
context,
|
||||
&mime_parser,
|
||||
@@ -391,7 +391,7 @@ unsafe fn add_parts(
|
||||
} else {
|
||||
MessageState::InFresh
|
||||
};
|
||||
*to_id = 1;
|
||||
*to_id = DC_CONTACT_ID_SELF;
|
||||
// handshake messages must be processed _before_ chats are created
|
||||
// (eg. contacs may be marked as verified)
|
||||
if mime_parser.lookup_field("Secure-Join").is_some() {
|
||||
@@ -551,8 +551,9 @@ unsafe fn add_parts(
|
||||
if to_ids.is_empty() && 0 != to_self {
|
||||
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
||||
// maybe an Autocrypt Setup Messag
|
||||
let (id, bl) = chat::create_or_lookup_by_contact_id(context, 1, Blocked::Not)
|
||||
.unwrap_or_default();
|
||||
let (id, bl) =
|
||||
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
|
||||
.unwrap_or_default();
|
||||
*chat_id = id;
|
||||
chat_id_blocked = bl;
|
||||
|
||||
@@ -604,6 +605,7 @@ unsafe fn add_parts(
|
||||
// into only one message; mails sent by other clients may result in several messages
|
||||
// (eg. one per attachment))
|
||||
let icnt = mime_parser.parts.len();
|
||||
|
||||
let mut txt_raw = None;
|
||||
|
||||
context.sql.prepare(
|
||||
@@ -644,17 +646,6 @@ unsafe fn add_parts(
|
||||
.set_int(Param::Cmd, mime_parser.is_system_message as i32);
|
||||
}
|
||||
|
||||
/*
|
||||
info!(
|
||||
context,
|
||||
"received mime message {:?}",
|
||||
String::from_utf8_lossy(std::slice::from_raw_parts(
|
||||
imf_raw_not_terminated as *const u8,
|
||||
imf_raw_bytes,
|
||||
))
|
||||
);
|
||||
*/
|
||||
|
||||
stmt.execute(params![
|
||||
rfc724_mid,
|
||||
server_folder.as_ref(),
|
||||
@@ -808,9 +799,9 @@ unsafe fn handle_reports(
|
||||
&& !of_org_msgid.is_null()
|
||||
&& !(*of_org_msgid).fld_value.is_null()
|
||||
{
|
||||
if let Ok(rfc724_mid) = wrapmime::parse_message_id(as_str(
|
||||
(*of_org_msgid).fld_value,
|
||||
)) {
|
||||
if let Ok(rfc724_mid) = wrapmime::parse_message_id(
|
||||
&to_string_lossy((*of_org_msgid).fld_value),
|
||||
) {
|
||||
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
|
||||
context,
|
||||
from_id,
|
||||
@@ -850,13 +841,16 @@ fn save_locations(
|
||||
insert_msg_id: MsgId,
|
||||
hidden: i32,
|
||||
) {
|
||||
if chat_id <= DC_CHAT_ID_LAST_SPECIAL {
|
||||
return ();
|
||||
}
|
||||
let mut location_id_written = false;
|
||||
let mut send_event = false;
|
||||
|
||||
if !mime_parser.message_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL as libc::c_uint {
|
||||
if mime_parser.message_kml.is_some() {
|
||||
let locations = &mime_parser.message_kml.as_ref().unwrap().locations;
|
||||
let newest_location_id =
|
||||
location::save(context, chat_id, from_id, locations, 1).unwrap_or_default();
|
||||
location::save(context, chat_id, from_id, locations, true).unwrap_or_default();
|
||||
if 0 != newest_location_id && 0 == hidden {
|
||||
if location::set_msg_location_id(context, insert_msg_id, newest_location_id).is_ok() {
|
||||
location_id_written = true;
|
||||
@@ -865,15 +859,14 @@ fn save_locations(
|
||||
}
|
||||
}
|
||||
|
||||
if !mime_parser.location_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL as libc::c_uint {
|
||||
if mime_parser.location_kml.is_some() {
|
||||
if let Some(ref addr) = mime_parser.location_kml.as_ref().unwrap().addr {
|
||||
if let Ok(contact) = Contact::get_by_id(context, from_id) {
|
||||
if !contact.get_addr().is_empty()
|
||||
&& contact.get_addr().to_lowercase() == addr.to_lowercase()
|
||||
{
|
||||
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
|
||||
let locations = &mime_parser.location_kml.as_ref().unwrap().locations;
|
||||
let newest_location_id =
|
||||
location::save(context, chat_id, from_id, locations, 0).unwrap_or_default();
|
||||
location::save(context, chat_id, from_id, locations, false)
|
||||
.unwrap_or_default();
|
||||
if newest_location_id != 0 && hidden == 0 && !location_id_written {
|
||||
if let Err(err) = location::set_msg_location_id(
|
||||
context,
|
||||
@@ -993,7 +986,7 @@ unsafe fn create_or_lookup_group(
|
||||
let fld_message_id = (*field).fld_data.fld_message_id;
|
||||
if !fld_message_id.is_null() {
|
||||
if let Some(extracted_grpid) =
|
||||
dc_extract_grpid_from_rfc724_mid(as_str((*fld_message_id).mid_value))
|
||||
dc_extract_grpid_from_rfc724_mid(&to_string_lossy((*fld_message_id).mid_value))
|
||||
{
|
||||
grpid = extracted_grpid.to_string();
|
||||
} else {
|
||||
@@ -1359,8 +1352,8 @@ unsafe fn create_or_lookup_adhoc_group(
|
||||
if !member_ids.contains(&from_id) {
|
||||
member_ids.push(from_id);
|
||||
}
|
||||
if !member_ids.contains(&1) {
|
||||
member_ids.push(1);
|
||||
if !member_ids.contains(&DC_CONTACT_ID_SELF) {
|
||||
member_ids.push(DC_CONTACT_ID_SELF);
|
||||
}
|
||||
if member_ids.len() < 3 {
|
||||
// too few contacts given
|
||||
@@ -1480,7 +1473,7 @@ fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
|
||||
.sql
|
||||
.query_map(
|
||||
format!(
|
||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1",
|
||||
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
|
||||
member_ids_str
|
||||
),
|
||||
params![],
|
||||
@@ -1534,7 +1527,7 @@ fn search_chat_ids_by_contact_ids(
|
||||
WHERE cc.chat_id IN(SELECT chat_id FROM chats_contacts WHERE contact_id IN({})) \
|
||||
AND c.type=120 \
|
||||
AND cc.contact_id!=1 \
|
||||
ORDER BY cc.chat_id, cc.contact_id;",
|
||||
ORDER BY cc.chat_id, cc.contact_id;", // 1=DC_CONTACT_ID_SELF
|
||||
contact_ids_str
|
||||
),
|
||||
params![],
|
||||
@@ -1731,7 +1724,7 @@ fn is_known_rfc724_mid(context: &Context, rfc724_mid: *const libc::c_char) -> li
|
||||
LEFT JOIN chats c ON m.chat_id=c.id \
|
||||
WHERE m.rfc724_mid=? \
|
||||
AND m.chat_id>9 AND c.blocked=0;",
|
||||
params![as_str(rfc724_mid)],
|
||||
params![to_string_lossy(rfc724_mid)],
|
||||
)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
@@ -1783,11 +1776,7 @@ unsafe fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: *const clis
|
||||
while !cur.is_null() {
|
||||
if 0 != is_msgrmsg_rfc724_mid(
|
||||
context,
|
||||
if !cur.is_null() {
|
||||
as_str((*cur).data as *const libc::c_char)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
&to_string_lossy((*cur).data as *const libc::c_char),
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
@@ -1923,7 +1912,7 @@ unsafe fn add_or_lookup_contact_by_addr(
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.unwrap_or_default();
|
||||
|
||||
if addr_cmp(self_addr, as_str(addr_spec)) {
|
||||
if addr_cmp(self_addr, to_string_lossy(addr_spec)) {
|
||||
*check_self = 1;
|
||||
}
|
||||
|
||||
@@ -1933,13 +1922,18 @@ unsafe fn add_or_lookup_contact_by_addr(
|
||||
/* add addr_spec if missing, update otherwise */
|
||||
let mut display_name_dec = "".to_string();
|
||||
if !display_name_enc.is_null() {
|
||||
let tmp = dc_decode_header_words(as_str(display_name_enc));
|
||||
let tmp = dc_decode_header_words(&to_string_lossy(display_name_enc));
|
||||
display_name_dec = normalize_name(&tmp);
|
||||
}
|
||||
/*can be NULL*/
|
||||
let row_id = Contact::add_or_lookup(context, display_name_dec, as_str(addr_spec), origin)
|
||||
.map(|(id, _)| id)
|
||||
.unwrap_or_default();
|
||||
let row_id = Contact::add_or_lookup(
|
||||
context,
|
||||
display_name_dec,
|
||||
to_string_lossy(addr_spec),
|
||||
origin,
|
||||
)
|
||||
.map(|(id, _)| id)
|
||||
.unwrap_or_default();
|
||||
if 0 != row_id && !ids.contains(&row_id) {
|
||||
ids.push(row_id);
|
||||
};
|
||||
|
||||
123
src/dc_tools.rs
123
src/dc_tools.rs
@@ -82,7 +82,7 @@ pub(crate) fn dc_str_from_clist(list: *const clist, delimiter: &str) -> String {
|
||||
if !res.is_empty() {
|
||||
res += delimiter;
|
||||
}
|
||||
res += as_str(rfc724_mid as *const libc::c_char);
|
||||
res += &to_string_lossy(rfc724_mid as *const libc::c_char);
|
||||
}
|
||||
}
|
||||
res
|
||||
@@ -249,12 +249,10 @@ pub(crate) fn dc_create_incoming_rfc724_mid(
|
||||
contact_id_from: u32,
|
||||
contact_ids_to: &[u32],
|
||||
) -> Option<String> {
|
||||
if contact_ids_to.is_empty() {
|
||||
return None;
|
||||
}
|
||||
/* find out the largest receiver ID (we could also take the smallest, but it should be unique) */
|
||||
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
|
||||
/* create a deterministic rfc724_mid from input such that
|
||||
repeatedly calling it with the same input results in the same Message-id */
|
||||
|
||||
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
|
||||
let result = format!(
|
||||
"{}-{}-{}@stub",
|
||||
message_timestamp, contact_id_from, largest_id_to
|
||||
@@ -303,9 +301,9 @@ pub(crate) fn dc_extract_grpid_from_rfc724_mid_list(list: *const clist) -> *mut
|
||||
if !list.is_null() {
|
||||
unsafe {
|
||||
for cur in (*list).into_iter() {
|
||||
let mid = as_str(cur as *const libc::c_char);
|
||||
let mid = to_string_lossy(cur as *const libc::c_char);
|
||||
|
||||
if let Some(grpid) = dc_extract_grpid_from_rfc724_mid(mid) {
|
||||
if let Some(grpid) = dc_extract_grpid_from_rfc724_mid(&mid) {
|
||||
return grpid.strdup();
|
||||
}
|
||||
}
|
||||
@@ -420,8 +418,8 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
|
||||
context.call_cb(Event::DeletedBlobFile(dpath));
|
||||
true
|
||||
}
|
||||
Err(_err) => {
|
||||
warn!(context, "Cannot delete \"{}\".", dpath);
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot delete \"{}\": {}", dpath, err);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -429,20 +427,55 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
|
||||
|
||||
pub(crate) fn dc_copy_file(
|
||||
context: &Context,
|
||||
src: impl AsRef<std::path::Path>,
|
||||
dest: impl AsRef<std::path::Path>,
|
||||
src_path: impl AsRef<std::path::Path>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> bool {
|
||||
let src_abs = dc_get_abs_path(context, &src);
|
||||
let dest_abs = dc_get_abs_path(context, &dest);
|
||||
match fs::copy(&src_abs, &dest_abs) {
|
||||
let src_abs = dc_get_abs_path(context, &src_path);
|
||||
let mut src_file = match fs::File::open(&src_abs) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to open for read '{}': {}",
|
||||
src_abs.display(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let dest_abs = dc_get_abs_path(context, &dest_path);
|
||||
let mut dest_file = match fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&dest_abs)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"failed to open for write '{}': {}",
|
||||
dest_abs.display(),
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match std::io::copy(&mut src_file, &mut dest_file) {
|
||||
Ok(_) => true,
|
||||
Err(_) => {
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Cannot copy \"{}\" to \"{}\".",
|
||||
src.as_ref().display(),
|
||||
dest.as_ref().display(),
|
||||
"Cannot copy \"{}\" to \"{}\": {}",
|
||||
src_abs.display(),
|
||||
dest_abs.display(),
|
||||
err
|
||||
);
|
||||
{
|
||||
// Attempt to remove the failed file, swallow errors resulting from that.
|
||||
fs::remove_file(dest_abs).ok();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -453,11 +486,12 @@ pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Pa
|
||||
if !path_abs.exists() {
|
||||
match fs::create_dir_all(path_abs) {
|
||||
Ok(_) => true,
|
||||
Err(_err) => {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot create directory \"{}\".",
|
||||
"Cannot create directory \"{}\": {}",
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
false
|
||||
}
|
||||
@@ -470,12 +504,13 @@ pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Pa
|
||||
/// Write a the given content to provied file path.
|
||||
pub(crate) fn dc_write_file(context: &Context, path: impl AsRef<Path>, buf: &[u8]) -> bool {
|
||||
let path_abs = dc_get_abs_path(context, &path);
|
||||
if let Err(_err) = fs::write(&path_abs, buf) {
|
||||
if let Err(err) = fs::write(&path_abs, buf) {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot write {} bytes to \"{}\".",
|
||||
"Cannot write {} bytes to \"{}\": {}",
|
||||
buf.len(),
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
false
|
||||
} else {
|
||||
@@ -494,8 +529,9 @@ pub fn dc_read_file<P: AsRef<std::path::Path>>(
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot read \"{}\" or file is empty.",
|
||||
path.as_ref().display()
|
||||
"Cannot read \"{}\" or file is empty: {}",
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
}
|
||||
@@ -513,8 +549,9 @@ pub fn dc_open_file<P: AsRef<std::path::Path>>(
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot read \"{}\" or file is empty.",
|
||||
path.as_ref().display()
|
||||
"Cannot read \"{}\" or file is empty: {}",
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
}
|
||||
@@ -704,27 +741,6 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||
Some(to_string_lossy(s))
|
||||
}
|
||||
|
||||
pub fn as_str<'a>(s: *const libc::c_char) -> &'a str {
|
||||
as_str_safe(s).unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Converts a C string to either a Rust `&str` or `None` if it is a null pointer.
|
||||
pub fn as_opt_str<'a>(s: *const libc::c_char) -> Option<&'a str> {
|
||||
if s.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(as_str(s))
|
||||
}
|
||||
|
||||
fn as_str_safe<'a>(s: *const libc::c_char) -> Result<&'a str, Error> {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
|
||||
let cstr = unsafe { CStr::from_ptr(s) };
|
||||
|
||||
cstr.to_str()
|
||||
.map_err(|err| format_err!("Non utf8 string: '{:?}' ({:?})", cstr.to_bytes(), err))
|
||||
}
|
||||
|
||||
/// Convert a C `*char` pointer to a [std::path::Path] slice.
|
||||
///
|
||||
/// This converts a `*libc::c_char` pointer to a [Path] slice. This
|
||||
@@ -761,7 +777,11 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
#[allow(dead_code)]
|
||||
fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||
std::path::Path::new(as_str(s))
|
||||
|
||||
let cstr = unsafe { CStr::from_ptr(s) };
|
||||
let str = cstr.to_str().unwrap_or_else(|err| panic!("{}", err));
|
||||
|
||||
std::path::Path::new(str)
|
||||
}
|
||||
|
||||
pub(crate) fn time() -> i64 {
|
||||
@@ -1244,6 +1264,8 @@ mod tests {
|
||||
fn test_dc_create_incoming_rfc724_mid() {
|
||||
let res = dc_create_incoming_rfc724_mid(123, 45, &vec![6, 7]);
|
||||
assert_eq!(res, Some("123-45-7@stub".into()));
|
||||
let res = dc_create_incoming_rfc724_mid(123, 45, &vec![]);
|
||||
assert_eq!(res, Some("123-45-0@stub".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1295,6 +1317,9 @@ mod tests {
|
||||
|
||||
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
|
||||
|
||||
// attempting to copy a second time should fail
|
||||
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
|
||||
|
||||
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7);
|
||||
|
||||
let buf = dc_read_file(context, "$BLOBDIR/dada").unwrap();
|
||||
|
||||
@@ -449,7 +449,7 @@ fn update_gossip_peerstates(
|
||||
|
||||
let optional_field = unsafe { *optional_field };
|
||||
if !optional_field.fld_name.is_null()
|
||||
&& as_str(optional_field.fld_name) == "Autocrypt-Gossip"
|
||||
&& to_string_lossy(optional_field.fld_name) == "Autocrypt-Gossip"
|
||||
{
|
||||
let value = to_string_lossy(optional_field.fld_value);
|
||||
let gossip_header = Aheader::from_str(&value);
|
||||
@@ -655,7 +655,7 @@ fn contains_report(mime: *mut Mailmime) -> bool {
|
||||
|
||||
if tp_type == MAILMIME_TYPE_COMPOSITE_TYPE as libc::c_int
|
||||
&& ct_type == MAILMIME_COMPOSITE_TYPE_MULTIPART as libc::c_int
|
||||
&& as_str(unsafe { (*mime.mm_content_type).ct_subtype }) == "report"
|
||||
&& to_string_lossy(unsafe { (*mime.mm_content_type).ct_subtype }) == "report"
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
232
src/imap.rs
232
src/imap.rs
@@ -1,8 +1,5 @@
|
||||
use std::net;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Condvar, Mutex, RwLock,
|
||||
};
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::configure::dc_connect_to_configured_imap;
|
||||
@@ -11,7 +8,7 @@ use crate::context::Context;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::error::Error;
|
||||
use crate::events::Event;
|
||||
use crate::job::{connect_to_inbox, job_add, Action};
|
||||
use crate::job::{connect_to_inbox, job_add_no_interrupt, Action};
|
||||
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam};
|
||||
use crate::message::{self, update_msg_move_state, update_server_uid};
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
@@ -36,14 +33,13 @@ const SELECT_ALL: &str = "1:*";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Imap {
|
||||
config: Arc<RwLock<ImapConfig>>,
|
||||
config: ImapConfig,
|
||||
watch: Arc<(Mutex<bool>, Condvar)>,
|
||||
|
||||
session: Arc<Mutex<Option<Session>>>,
|
||||
stream: Arc<RwLock<Option<net::TcpStream>>>,
|
||||
connected: Arc<Mutex<bool>>,
|
||||
|
||||
should_reconnect: AtomicBool,
|
||||
stream: Option<net::TcpStream>,
|
||||
connected: bool,
|
||||
should_reconnect: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -361,27 +357,27 @@ impl Default for ImapConfig {
|
||||
}
|
||||
|
||||
impl Imap {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(watch: Arc<(Mutex<bool>, Condvar)>) -> Self {
|
||||
Imap {
|
||||
session: Arc::new(Mutex::new(None)),
|
||||
stream: Arc::new(RwLock::new(None)),
|
||||
config: Arc::new(RwLock::new(ImapConfig::default())),
|
||||
watch: Arc::new((Mutex::new(false), Condvar::new())),
|
||||
connected: Arc::new(Mutex::new(false)),
|
||||
should_reconnect: AtomicBool::new(false),
|
||||
stream: None,
|
||||
config: ImapConfig::default(),
|
||||
watch,
|
||||
connected: false,
|
||||
should_reconnect: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
*self.connected.lock().unwrap()
|
||||
self.connected
|
||||
}
|
||||
|
||||
pub fn should_reconnect(&self) -> bool {
|
||||
self.should_reconnect.load(Ordering::Relaxed)
|
||||
self.should_reconnect
|
||||
}
|
||||
|
||||
fn setup_handle_if_needed(&self, context: &Context) -> bool {
|
||||
if self.config.read().unwrap().imap_server.is_empty() {
|
||||
fn setup_handle_if_needed(&mut self, context: &Context) -> bool {
|
||||
if self.config.imap_server.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -389,16 +385,16 @@ impl Imap {
|
||||
self.unsetup_handle(context);
|
||||
}
|
||||
|
||||
if self.is_connected() && self.stream.read().unwrap().is_some() {
|
||||
self.should_reconnect.store(false, Ordering::Relaxed);
|
||||
if self.is_connected() && self.stream.is_some() {
|
||||
self.should_reconnect = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
let server_flags = self.config.read().unwrap().server_flags as i32;
|
||||
let server_flags = self.config.server_flags as i32;
|
||||
|
||||
let connection_res: imap::error::Result<Client> =
|
||||
if (server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN)) != 0 {
|
||||
let config = self.config.read().unwrap();
|
||||
let config = &self.config;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
|
||||
@@ -410,7 +406,7 @@ impl Imap {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let config = self.config.read().unwrap();
|
||||
let config = &self.config;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
|
||||
@@ -423,7 +419,7 @@ impl Imap {
|
||||
|
||||
let login_res = match connection_res {
|
||||
Ok(client) => {
|
||||
let config = self.config.read().unwrap();
|
||||
let config = &self.config;
|
||||
let imap_user: &str = config.imap_user.as_ref();
|
||||
let imap_pw: &str = config.imap_pw.as_ref();
|
||||
|
||||
@@ -444,7 +440,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let config = self.config.read().unwrap();
|
||||
let config = &self.config;
|
||||
let imap_server: &str = config.imap_server.as_ref();
|
||||
let imap_port = config.imap_port;
|
||||
let message = context.stock_string_repl_str2(
|
||||
@@ -459,17 +455,17 @@ impl Imap {
|
||||
}
|
||||
};
|
||||
|
||||
self.should_reconnect.store(false, Ordering::Relaxed);
|
||||
self.should_reconnect = false;
|
||||
|
||||
match login_res {
|
||||
Ok((session, stream)) => {
|
||||
*self.session.lock().unwrap() = Some(session);
|
||||
*self.stream.write().unwrap() = Some(stream);
|
||||
self.stream = Some(stream);
|
||||
true
|
||||
}
|
||||
Err((err, _)) => {
|
||||
let imap_user = self.config.read().unwrap().imap_user.to_owned();
|
||||
let message = context.stock_string_repl_str(StockMessage::CannotLogin, &imap_user);
|
||||
let imap_user = &self.config.imap_user;
|
||||
let message = context.stock_string_repl_str(StockMessage::CannotLogin, imap_user);
|
||||
|
||||
emit_event!(
|
||||
context,
|
||||
@@ -482,10 +478,10 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
fn unsetup_handle(&self, context: &Context) {
|
||||
fn unsetup_handle(&mut self, context: &Context) {
|
||||
info!(context, "IMAP unsetup_handle step 1 (closing down stream).");
|
||||
let stream = self.stream.write().unwrap().take();
|
||||
if let Some(stream) = stream {
|
||||
|
||||
if let Some(stream) = self.stream.take() {
|
||||
if let Err(err) = stream.shutdown(net::Shutdown::Both) {
|
||||
warn!(context, "failed to shutdown connection: {:?}", err);
|
||||
}
|
||||
@@ -502,13 +498,13 @@ impl Imap {
|
||||
}
|
||||
|
||||
info!(context, "IMAP unsetup_handle step 3 (clearing config).");
|
||||
self.config.write().unwrap().selected_folder = None;
|
||||
self.config.write().unwrap().selected_mailbox = None;
|
||||
self.config.selected_folder = None;
|
||||
self.config.selected_mailbox = None;
|
||||
info!(context, "IMAP unsetup_handle step 4 (disconnected).",);
|
||||
}
|
||||
|
||||
fn free_connect_params(&self) {
|
||||
let mut cfg = self.config.write().unwrap();
|
||||
fn free_connect_params(&mut self) {
|
||||
let mut cfg = &mut self.config;
|
||||
|
||||
cfg.addr = "".into();
|
||||
cfg.imap_server = "".into();
|
||||
@@ -522,7 +518,7 @@ impl Imap {
|
||||
cfg.watch_folder = None;
|
||||
}
|
||||
|
||||
pub fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
|
||||
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool {
|
||||
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
|
||||
return false;
|
||||
}
|
||||
@@ -539,7 +535,7 @@ impl Imap {
|
||||
let imap_pw = &lp.mail_pw;
|
||||
let server_flags = lp.server_flags as usize;
|
||||
|
||||
let mut config = self.config.write().unwrap();
|
||||
let mut config = &mut self.config;
|
||||
config.addr = addr.to_string();
|
||||
config.imap_server = imap_server.to_string();
|
||||
config.imap_port = imap_port;
|
||||
@@ -587,41 +583,43 @@ impl Imap {
|
||||
if teardown {
|
||||
self.unsetup_handle(context);
|
||||
self.free_connect_params();
|
||||
self.connected = false;
|
||||
false
|
||||
} else {
|
||||
self.config.write().unwrap().can_idle = can_idle;
|
||||
self.config.write().unwrap().has_xlist = has_xlist;
|
||||
*self.connected.lock().unwrap() = true;
|
||||
self.config.can_idle = can_idle;
|
||||
self.config.has_xlist = has_xlist;
|
||||
self.connected = true;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(&self, context: &Context) {
|
||||
if self.is_connected() {
|
||||
self.unsetup_handle(context);
|
||||
self.free_connect_params();
|
||||
*self.connected.lock().unwrap() = false;
|
||||
}
|
||||
pub fn disconnect(&mut self, context: &Context) {
|
||||
// if self.is_connected() {
|
||||
info!(context, "disconnecting imap connection");
|
||||
self.unsetup_handle(context);
|
||||
self.free_connect_params();
|
||||
// }
|
||||
self.connected = false;
|
||||
}
|
||||
|
||||
pub fn set_watch_folder(&self, watch_folder: String) {
|
||||
self.config.write().unwrap().watch_folder = Some(watch_folder);
|
||||
pub fn set_watch_folder(&mut self, watch_folder: String) {
|
||||
self.config.watch_folder = Some(watch_folder);
|
||||
}
|
||||
|
||||
pub fn fetch(&self, context: &Context) -> bool {
|
||||
pub fn fetch(&mut self, context: &Context) -> bool {
|
||||
if !self.is_connected() || !context.sql.is_open() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.setup_handle_if_needed(context);
|
||||
|
||||
let watch_folder = self.config.read().unwrap().watch_folder.to_owned();
|
||||
|
||||
let watch_folder = self.config.watch_folder.clone();
|
||||
if let Some(ref watch_folder) = watch_folder {
|
||||
// as during the fetch commands, new messages may arrive, we fetch until we do not
|
||||
// get any more. if IDLE is called directly after, there is only a small chance that
|
||||
// messages are missed and delayed until the next IDLE call
|
||||
loop {
|
||||
info!(context, "imap: fetching single folder");
|
||||
if self.fetch_from_single_folder(context, watch_folder) == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -632,9 +630,10 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_folder<S: AsRef<str>>(&self, context: &Context, folder: Option<S>) -> usize {
|
||||
if self.session.lock().unwrap().is_none() {
|
||||
let mut cfg = self.config.write().unwrap();
|
||||
fn select_folder<S: AsRef<str>>(&mut self, context: &Context, folder: Option<S>) -> usize {
|
||||
info!(context, "select folder, waiting for session lock");
|
||||
if !self.is_connected() {
|
||||
let mut cfg = &mut self.config;
|
||||
cfg.selected_folder = None;
|
||||
cfg.selected_folder_needs_expunge = false;
|
||||
return 0;
|
||||
@@ -643,7 +642,7 @@ impl Imap {
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(ref folder) = folder {
|
||||
if let Some(ref selected_folder) = self.config.read().unwrap().selected_folder {
|
||||
if let Some(ref selected_folder) = self.config.selected_folder {
|
||||
if folder.as_ref() == selected_folder {
|
||||
return 1;
|
||||
}
|
||||
@@ -651,9 +650,13 @@ impl Imap {
|
||||
}
|
||||
|
||||
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
|
||||
let needs_expunge = { self.config.read().unwrap().selected_folder_needs_expunge };
|
||||
let needs_expunge: bool = self.config.selected_folder_needs_expunge;
|
||||
if needs_expunge {
|
||||
if let Some(ref folder) = self.config.read().unwrap().selected_folder {
|
||||
if !self.is_connected() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if let Some(ref folder) = self.config.selected_folder {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
|
||||
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
|
||||
@@ -672,15 +675,19 @@ impl Imap {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
self.config.write().unwrap().selected_folder_needs_expunge = false;
|
||||
self.config.selected_folder_needs_expunge = false;
|
||||
}
|
||||
|
||||
// select new folder
|
||||
if let Some(ref folder) = folder {
|
||||
if !self.is_connected() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if let Some(ref mut session) = &mut *self.session.lock().unwrap() {
|
||||
match session.select(folder) {
|
||||
Ok(mailbox) => {
|
||||
let mut config = self.config.write().unwrap();
|
||||
let mut config = &mut self.config;
|
||||
config.selected_folder = Some(folder.as_ref().to_string());
|
||||
config.selected_mailbox = Some(mailbox);
|
||||
}
|
||||
@@ -692,13 +699,13 @@ impl Imap {
|
||||
err
|
||||
);
|
||||
|
||||
self.config.write().unwrap().selected_folder = None;
|
||||
self.should_reconnect.store(true, Ordering::Relaxed);
|
||||
self.config.selected_folder = None;
|
||||
self.should_reconnect = true;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +734,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_from_single_folder<S: AsRef<str>>(&self, context: &Context, folder: S) -> usize {
|
||||
fn fetch_from_single_folder<S: AsRef<str>>(&mut self, context: &Context, folder: S) -> usize {
|
||||
if !self.is_connected() {
|
||||
info!(
|
||||
context,
|
||||
@@ -738,7 +745,10 @@ impl Imap {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if self.select_folder(context, Some(&folder)) == 0 {
|
||||
info!(context, "selecting folder");
|
||||
let r = self.select_folder(context, Some(&folder));
|
||||
info!(context, "selecting folder done {}", r);
|
||||
if r == 0 {
|
||||
info!(
|
||||
context,
|
||||
"Cannot select folder \"{}\" for fetching.",
|
||||
@@ -751,8 +761,11 @@ impl Imap {
|
||||
// compare last seen UIDVALIDITY against the current one
|
||||
let (mut uid_validity, mut last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
|
||||
|
||||
let config = self.config.read().unwrap();
|
||||
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
|
||||
let mailbox = self
|
||||
.config
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.expect("just selected");
|
||||
|
||||
if mailbox.uid_validity.is_none() {
|
||||
error!(
|
||||
@@ -784,12 +797,17 @@ impl Imap {
|
||||
}
|
||||
|
||||
let list = if let Some(ref mut session) = &mut *self.session.lock().unwrap() {
|
||||
if !self.is_connected() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `FETCH <message sequence number> (UID)`
|
||||
let set = format!("{}", mailbox.exists);
|
||||
info!(context, "session fetch {}", &set);
|
||||
match session.fetch(set, PREFETCH_FLAGS) {
|
||||
Ok(list) => list,
|
||||
Err(_err) => {
|
||||
self.should_reconnect.store(true, Ordering::Relaxed);
|
||||
self.should_reconnect = true;
|
||||
info!(
|
||||
context,
|
||||
"No result returned for folder \"{}\".",
|
||||
@@ -826,6 +844,9 @@ impl Imap {
|
||||
let mut new_last_seen_uid = 0;
|
||||
|
||||
let list = if let Some(ref mut session) = &mut *self.session.lock().unwrap() {
|
||||
if !self.is_connected() {
|
||||
return 0;
|
||||
}
|
||||
// fetch messages with larger UID than the last one seen
|
||||
// (`UID FETCH lastseenuid+1:*)`, see RFC 4549
|
||||
let set = format!("{}:*", last_seen_uid + 1);
|
||||
@@ -915,7 +936,7 @@ impl Imap {
|
||||
}
|
||||
|
||||
fn fetch_single_msg<S: AsRef<str>>(
|
||||
&self,
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: S,
|
||||
server_uid: u32,
|
||||
@@ -933,7 +954,7 @@ impl Imap {
|
||||
match session.uid_fetch(set, BODY_FLAGS) {
|
||||
Ok(msgs) => msgs,
|
||||
Err(err) => {
|
||||
self.should_reconnect.store(true, Ordering::Relaxed);
|
||||
self.should_reconnect = true;
|
||||
warn!(
|
||||
context,
|
||||
"Error on fetching message #{} from folder \"{}\"; retry={}; error={}.",
|
||||
@@ -982,15 +1003,16 @@ impl Imap {
|
||||
1
|
||||
}
|
||||
|
||||
pub fn idle(&self, context: &Context) {
|
||||
if !self.config.read().unwrap().can_idle {
|
||||
pub fn idle(&mut self, context: &Context) {
|
||||
info!(context, "IDLE START");
|
||||
if !self.config.can_idle {
|
||||
return self.fake_idle(context);
|
||||
}
|
||||
|
||||
self.setup_handle_if_needed(context);
|
||||
|
||||
let watch_folder = self.config.read().unwrap().watch_folder.clone();
|
||||
if self.select_folder(context, watch_folder.as_ref()) == 0 {
|
||||
let watch_folder = self.config.watch_folder.clone();
|
||||
if self.select_folder(context, watch_folder) == 0 {
|
||||
warn!(context, "IMAP-IDLE not setup.",);
|
||||
|
||||
return self.fake_idle(context);
|
||||
@@ -1047,7 +1069,7 @@ impl Imap {
|
||||
let &(ref lock, ref cvar) = &*self.watch.clone();
|
||||
let mut watch = lock.lock().unwrap();
|
||||
|
||||
let handle_res = |res| match res {
|
||||
let mut handle_res = |res| match res {
|
||||
Ok(()) => {
|
||||
info!(context, "IMAP-IDLE has data.");
|
||||
}
|
||||
@@ -1058,7 +1080,7 @@ impl Imap {
|
||||
info!(context, "IMAP-IDLE wait cancelled, we will reconnect soon.");
|
||||
self.unsetup_handle(context);
|
||||
info!(context, "IMAP-IDLE has SHUTDOWN");
|
||||
self.should_reconnect.store(true, Ordering::Relaxed);
|
||||
self.should_reconnect = true;
|
||||
}
|
||||
_ => {
|
||||
warn!(context, "IMAP-IDLE returns unknown value: {}", err);
|
||||
@@ -1074,22 +1096,25 @@ impl Imap {
|
||||
let res = cvar.wait(watch).unwrap();
|
||||
watch = res;
|
||||
if *watch {
|
||||
if let Ok(res) = worker.as_ref().unwrap().try_recv() {
|
||||
let msg = worker.as_ref().unwrap().try_recv();
|
||||
drop(worker.take());
|
||||
|
||||
if let Ok(res) = msg {
|
||||
handle_res(res);
|
||||
} else {
|
||||
info!(context, "IMAP-IDLE interrupted");
|
||||
}
|
||||
|
||||
drop(worker.take());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*watch = false;
|
||||
|
||||
info!(context, "IDLE STOP");
|
||||
}
|
||||
|
||||
fn fake_idle(&self, context: &Context) {
|
||||
fn fake_idle(&mut self, context: &Context) {
|
||||
// Idle using timeouts. This is also needed if we're not yet configured -
|
||||
// in this case, we're waiting for a configure job
|
||||
let fake_idle_start_time = SystemTime::now();
|
||||
@@ -1134,7 +1159,7 @@ impl Imap {
|
||||
// try to connect with proper login params
|
||||
// (setup_handle_if_needed might not know about them if we
|
||||
// never successfully connected)
|
||||
if dc_connect_to_configured_imap(context, &self) != 0 {
|
||||
if dc_connect_to_configured_imap(context, self) != 0 {
|
||||
return;
|
||||
}
|
||||
// we cannot connect, wait long next time (currently 60 secs, see above)
|
||||
@@ -1146,8 +1171,7 @@ impl Imap {
|
||||
// will have already fetched the messages so perform_*_fetch
|
||||
// will not find any new.
|
||||
|
||||
let watch_folder = self.config.read().unwrap().watch_folder.clone();
|
||||
if let Some(watch_folder) = watch_folder {
|
||||
if let Some(ref watch_folder) = self.config.watch_folder.clone() {
|
||||
if 0 != self.fetch_from_single_folder(context, watch_folder) {
|
||||
do_fake_idle = false;
|
||||
}
|
||||
@@ -1155,17 +1179,17 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_idle(&self) {
|
||||
// interrupt idle
|
||||
let &(ref lock, ref cvar) = &*self.watch.clone();
|
||||
let mut watch = lock.lock().unwrap();
|
||||
// pub fn interrupt_idle(&self) {
|
||||
// // interrupt idle
|
||||
// let &(ref lock, ref cvar) = &*self.watch.clone();
|
||||
// let mut watch = lock.lock().unwrap();
|
||||
|
||||
*watch = true;
|
||||
cvar.notify_one();
|
||||
}
|
||||
// *watch = true;
|
||||
// cvar.notify_one();
|
||||
// }
|
||||
|
||||
pub fn mv(
|
||||
&self,
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
@@ -1223,7 +1247,7 @@ impl Imap {
|
||||
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
|
||||
ImapResult::Failed
|
||||
} else {
|
||||
self.config.write().unwrap().selected_folder_needs_expunge = true;
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
ImapResult::Success
|
||||
}
|
||||
}
|
||||
@@ -1272,7 +1296,7 @@ impl Imap {
|
||||
}
|
||||
|
||||
pub fn prepare_imap_operation_on_msg(
|
||||
&self,
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
@@ -1280,7 +1304,7 @@ impl Imap {
|
||||
if uid == 0 {
|
||||
return Some(ImapResult::Failed);
|
||||
} else if !self.is_connected() {
|
||||
connect_to_inbox(context, &self);
|
||||
connect_to_inbox(context, self);
|
||||
if !self.is_connected() {
|
||||
return Some(ImapResult::RetryLater);
|
||||
}
|
||||
@@ -1296,7 +1320,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_seen(&self, context: &Context, folder: &str, uid: u32) -> ImapResult {
|
||||
pub fn set_seen(&mut self, context: &Context, folder: &str, uid: u32) -> ImapResult {
|
||||
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) {
|
||||
return imapresult;
|
||||
}
|
||||
@@ -1316,7 +1340,7 @@ impl Imap {
|
||||
|
||||
// only returns 0 on connection problems; we should try later again in this case *
|
||||
pub fn delete_msg(
|
||||
&self,
|
||||
&mut self,
|
||||
context: &Context,
|
||||
message_id: &str,
|
||||
folder: &str,
|
||||
@@ -1383,12 +1407,12 @@ impl Imap {
|
||||
display_imap_id, message_id
|
||||
))
|
||||
);
|
||||
self.config.write().unwrap().selected_folder_needs_expunge = true;
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
ImapResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_folders(&self, context: &Context, flags: libc::c_int) {
|
||||
pub fn configure_folders(&mut self, context: &Context, flags: libc::c_int) {
|
||||
if !self.is_connected() {
|
||||
return;
|
||||
}
|
||||
@@ -1396,7 +1420,7 @@ impl Imap {
|
||||
info!(context, "Configuring IMAP-folders.");
|
||||
|
||||
let folders = self.list_folders(context).unwrap();
|
||||
let delimiter = self.config.read().unwrap().imap_delimiter;
|
||||
let delimiter = self.config.imap_delimiter;
|
||||
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
|
||||
|
||||
let mut mvbox_folder = folders
|
||||
@@ -1498,7 +1522,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_folder(&self, context: &Context, folder: &str) {
|
||||
pub fn empty_folder(&mut self, context: &Context, folder: &str) {
|
||||
info!(context, "emptying folder {}", folder);
|
||||
|
||||
if folder.is_empty() || self.select_folder(context, Some(&folder)) == 0 {
|
||||
@@ -1510,7 +1534,7 @@ impl Imap {
|
||||
warn!(context, "Cannot empty folder {}", folder);
|
||||
} else {
|
||||
// we now trigger expunge to actually delete messages
|
||||
self.config.write().unwrap().selected_folder_needs_expunge = true;
|
||||
self.config.selected_folder_needs_expunge = true;
|
||||
if self.select_folder::<String>(context, None) == 0 {
|
||||
warn!(
|
||||
context,
|
||||
@@ -1573,7 +1597,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
|
||||
{
|
||||
if old_server_folder.is_empty() && old_server_uid == 0 {
|
||||
info!(context, "[move] detected bbc-self {}", rfc724_mid,);
|
||||
job_add(
|
||||
job_add_no_interrupt(
|
||||
context,
|
||||
Action::MarkseenMsgOnImap,
|
||||
msg_id.to_u32() as i32,
|
||||
|
||||
51
src/imex.rs
51
src/imex.rs
@@ -1,5 +1,5 @@
|
||||
use core::cmp::{max, min};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use rand::{thread_rng, Rng};
|
||||
@@ -75,7 +75,7 @@ pub fn imex(context: &Context, what: ImexMode, param1: Option<impl AsRef<Path>>)
|
||||
job_add(context, Action::ImexImap, 0, param, 0);
|
||||
}
|
||||
|
||||
/// Returns the filename of the backup if found, nullptr otherwise.
|
||||
/// Returns the filename of the backup found (otherwise an error)
|
||||
pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
|
||||
let dir_name = dir_name.as_ref();
|
||||
let dir_iter = std::fs::read_dir(dir_name)?;
|
||||
@@ -90,13 +90,15 @@ pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<Strin
|
||||
if name.starts_with("delta-chat") && name.ends_with(".bak") {
|
||||
let sql = Sql::new();
|
||||
if sql.open(context, &path, true) {
|
||||
let curr_backup_time =
|
||||
sql.get_raw_config_int(context, "backup_time")
|
||||
.unwrap_or_default() as u64;
|
||||
let curr_backup_time = sql
|
||||
.get_raw_config_int(context, "backup_time")
|
||||
.unwrap_or_default();
|
||||
if curr_backup_time > newest_backup_time {
|
||||
newest_backup_path = Some(path);
|
||||
newest_backup_time = curr_backup_time;
|
||||
}
|
||||
info!(context, "backup_time of {} is {}", name, curr_backup_time);
|
||||
sql.close(&context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +107,7 @@ pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<Strin
|
||||
}
|
||||
match newest_backup_path {
|
||||
Some(path) => Ok(path.to_string_lossy().into_owned()),
|
||||
None => bail!("no backup found"),
|
||||
None => bail!("no backup found in {}", dir_name.display()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,51 +485,56 @@ fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
|
||||
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
|
||||
let now = time();
|
||||
let dest_path_filename = dc_get_next_backup_path(dir, now)?;
|
||||
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
|
||||
|
||||
sql::housekeeping(context);
|
||||
|
||||
sql::try_execute(context, &context.sql, "VACUUM;").ok();
|
||||
|
||||
// we close the database during the copy of the dbfile
|
||||
context.sql.close(context);
|
||||
info!(
|
||||
context,
|
||||
"Backup \"{}\" to \"{}\".",
|
||||
"Backup '{}' to '{}'.",
|
||||
context.get_dbfile().display(),
|
||||
dest_path_filename.display(),
|
||||
);
|
||||
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename);
|
||||
context.sql.open(&context, &context.get_dbfile(), false);
|
||||
|
||||
if !copied {
|
||||
let s = dest_path_filename.to_string_lossy().to_string();
|
||||
bail!(
|
||||
"could not copy file from {:?} to {:?}",
|
||||
context.get_dbfile(),
|
||||
s
|
||||
"could not copy file from '{}' to '{}'",
|
||||
context.get_dbfile().display(),
|
||||
dest_path_string
|
||||
);
|
||||
}
|
||||
match add_files_to_export(context, &dest_path_filename) {
|
||||
let dest_sql = Sql::new();
|
||||
ensure!(
|
||||
dest_sql.open(context, &dest_path_filename, false),
|
||||
"could not open exported database {}",
|
||||
dest_path_string
|
||||
);
|
||||
let res = match add_files_to_export(context, &dest_sql) {
|
||||
Err(err) => {
|
||||
dc_delete_file(context, &dest_path_filename);
|
||||
error!(context, "backup failed: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
Ok(()) => {
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int(context, "backup_time", now as i32)?;
|
||||
dest_sql.set_raw_config_int(context, "backup_time", now as i32)?;
|
||||
context.call_cb(Event::ImexFileWritten(dest_path_filename.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
dest_sql.close(context);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn add_files_to_export(context: &Context, dest_path_filename: &PathBuf) -> Result<()> {
|
||||
fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
|
||||
// add all files as blobs to the database copy (this does not require
|
||||
// the source to be locked, neigher the destination as it is used only here)
|
||||
let sql = Sql::new();
|
||||
ensure!(
|
||||
sql.open(context, &dest_path_filename, false),
|
||||
"could not open db"
|
||||
);
|
||||
if !sql.table_exists("backup_blobs") {
|
||||
sql::execute(
|
||||
context,
|
||||
|
||||
82
src/job.rs
82
src/job.rs
@@ -135,8 +135,7 @@ impl Job {
|
||||
if !context.smtp.lock().unwrap().is_connected() {
|
||||
let loginparam = LoginParam::from_database(context, "configured_");
|
||||
let connected = context.smtp.lock().unwrap().connect(context, &loginparam);
|
||||
|
||||
if !connected {
|
||||
if connected.is_err() {
|
||||
self.try_again_later(3, None);
|
||||
return;
|
||||
}
|
||||
@@ -173,10 +172,10 @@ impl Job {
|
||||
// its ok/error response processing. Note that if a message
|
||||
// was sent we need to mark it in the database ASAP as we
|
||||
// otherwise might send it twice.
|
||||
let mut sock = context.smtp.lock().unwrap();
|
||||
match sock.send(context, recipients_list, body) {
|
||||
let mut smtp = context.smtp.lock().unwrap();
|
||||
match smtp.send(context, recipients_list, body, self.job_id) {
|
||||
Err(err) => {
|
||||
sock.disconnect();
|
||||
smtp.disconnect();
|
||||
warn!(context, "smtp failed: {}", err);
|
||||
self.try_again_later(-1, Some(err.to_string()));
|
||||
}
|
||||
@@ -220,7 +219,7 @@ impl Job {
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn do_DC_JOB_MOVE_MSG(&mut self, context: &Context) {
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
|
||||
if let Ok(msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
if context
|
||||
@@ -265,7 +264,7 @@ impl Job {
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn do_DC_JOB_DELETE_MSG_ON_IMAP(&mut self, context: &Context) {
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
|
||||
if let Ok(mut msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
@@ -293,7 +292,7 @@ impl Job {
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn do_DC_JOB_EMPTY_SERVER(&mut self, context: &Context) {
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
if self.foreign_id & DC_EMPTY_MVBOX > 0 {
|
||||
if let Some(mvbox_folder) = context
|
||||
.sql
|
||||
@@ -309,7 +308,7 @@ impl Job {
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn do_DC_JOB_MARKSEEN_MSG_ON_IMAP(&mut self, context: &Context) {
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
|
||||
if let Ok(msg) = Message::load_from_db(context, MsgId::new(self.foreign_id)) {
|
||||
let folder = msg.server_folder.as_ref().unwrap();
|
||||
@@ -343,7 +342,7 @@ impl Job {
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let uid = self.param.get_int(Param::ServerUid).unwrap_or_default() as u32;
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
if inbox.set_seen(context, &folder, uid) == ImapResult::RetryLater {
|
||||
self.try_again_later(3i32, None);
|
||||
return;
|
||||
@@ -384,10 +383,10 @@ pub fn job_kill_action(context: &Context, action: Action) -> bool {
|
||||
}
|
||||
|
||||
pub fn perform_imap_fetch(context: &Context) {
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
if 0 == connect_to_inbox(context, &inbox) {
|
||||
if 0 == connect_to_inbox(context, &mut inbox) {
|
||||
return;
|
||||
}
|
||||
if !context.get_config_bool(Config::InboxWatch) {
|
||||
@@ -403,14 +402,14 @@ pub fn perform_imap_fetch(context: &Context) {
|
||||
info!(
|
||||
context,
|
||||
"INBOX-fetch done in {:.4} ms.",
|
||||
start.elapsed().as_nanos() as f64 / 1000.0,
|
||||
start.elapsed().as_nanos() as f64 / 1_000_000.0,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn perform_imap_idle(context: &Context) {
|
||||
let inbox = context.inbox.read().unwrap();
|
||||
let mut inbox = context.inbox.write().unwrap();
|
||||
|
||||
connect_to_inbox(context, &inbox);
|
||||
connect_to_inbox(context, &mut inbox);
|
||||
|
||||
if *context.perform_inbox_jobs_needed.clone().read().unwrap() {
|
||||
info!(
|
||||
@@ -419,8 +418,11 @@ pub fn perform_imap_idle(context: &Context) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
drop(inbox);
|
||||
info!(context, "INBOX-IDLE started...");
|
||||
inbox.idle(context);
|
||||
|
||||
context.inbox.write().unwrap().idle(context);
|
||||
|
||||
info!(context, "INBOX-IDLE ended.");
|
||||
}
|
||||
|
||||
@@ -439,7 +441,7 @@ pub fn perform_mvbox_idle(context: &Context) {
|
||||
|
||||
context
|
||||
.mvbox_thread
|
||||
.read()
|
||||
.write()
|
||||
.unwrap()
|
||||
.idle(context, use_network);
|
||||
}
|
||||
@@ -463,7 +465,7 @@ pub fn perform_sentbox_idle(context: &Context) {
|
||||
|
||||
context
|
||||
.sentbox_thread
|
||||
.read()
|
||||
.write()
|
||||
.unwrap()
|
||||
.idle(context, use_network);
|
||||
}
|
||||
@@ -927,7 +929,7 @@ fn suspend_smtp_thread(context: &Context, suspend: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_to_inbox(context: &Context, inbox: &Imap) -> libc::c_int {
|
||||
pub fn connect_to_inbox(context: &Context, inbox: &mut Imap) -> libc::c_int {
|
||||
let ret_connected = dc_connect_to_configured_imap(context, inbox);
|
||||
if 0 != ret_connected {
|
||||
inbox.set_watch_folder("INBOX".into());
|
||||
@@ -981,6 +983,24 @@ pub fn job_add(
|
||||
foreign_id: libc::c_int,
|
||||
param: Params,
|
||||
delay_seconds: i64,
|
||||
) {
|
||||
job_add_no_interrupt(context, action, foreign_id, param, delay_seconds);
|
||||
|
||||
let thread: Thread = action.into();
|
||||
|
||||
match thread {
|
||||
Thread::Imap => interrupt_imap_idle(context),
|
||||
Thread::Smtp => interrupt_smtp_idle(context),
|
||||
Thread::Unknown => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn job_add_no_interrupt(
|
||||
context: &Context,
|
||||
action: Action,
|
||||
foreign_id: libc::c_int,
|
||||
param: Params,
|
||||
delay_seconds: i64,
|
||||
) {
|
||||
if action == Action::Unknown {
|
||||
error!(context, "Invalid action passed to job_add");
|
||||
@@ -1003,28 +1023,30 @@ pub fn job_add(
|
||||
(timestamp + delay_seconds as i64)
|
||||
]
|
||||
).ok();
|
||||
|
||||
match thread {
|
||||
Thread::Imap => interrupt_imap_idle(context),
|
||||
Thread::Smtp => interrupt_smtp_idle(context),
|
||||
Thread::Unknown => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_smtp_idle(context: &Context) {
|
||||
info!(context, "Interrupting SMTP-idle...",);
|
||||
|
||||
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
{
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
state.perform_jobs_needed = 1;
|
||||
state.idle = true;
|
||||
state.perform_jobs_needed = 1;
|
||||
state.idle = true;
|
||||
info!(context, "smtp interrupt jobs written");
|
||||
}
|
||||
cvar.notify_one();
|
||||
info!(context, "smtp interrupt done");
|
||||
}
|
||||
|
||||
pub fn interrupt_imap_idle(context: &Context) {
|
||||
info!(context, "Interrupting IMAP-IDLE...",);
|
||||
info!(context, "Interrupting INBOX-IDLE...",);
|
||||
|
||||
*context.perform_inbox_jobs_needed.write().unwrap() = true;
|
||||
context.inbox.read().unwrap().interrupt_idle();
|
||||
info!(context, "interrupt jobs written");
|
||||
|
||||
context.interrupt_inbox_idle();
|
||||
|
||||
info!(context, "interrupt imap done");
|
||||
}
|
||||
|
||||
@@ -9,23 +9,28 @@ pub struct JobThread {
|
||||
pub name: &'static str,
|
||||
pub folder_config_name: &'static str,
|
||||
pub imap: Imap,
|
||||
watch: Arc<(Mutex<bool>, Condvar)>,
|
||||
pub state: Arc<(Mutex<JobState>, Condvar)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct JobState {
|
||||
idle: bool,
|
||||
jobs_needed: i32,
|
||||
jobs_needed: bool,
|
||||
suspended: bool,
|
||||
using_handle: bool,
|
||||
}
|
||||
|
||||
impl JobThread {
|
||||
pub fn new(name: &'static str, folder_config_name: &'static str, imap: Imap) -> Self {
|
||||
pub fn new(name: &'static str, folder_config_name: &'static str) -> Self {
|
||||
let watch = Arc::new((Mutex::new(false), Condvar::new()));
|
||||
let imap = Imap::new(watch.clone());
|
||||
|
||||
JobThread {
|
||||
name,
|
||||
folder_config_name,
|
||||
imap,
|
||||
watch,
|
||||
state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
|
||||
}
|
||||
}
|
||||
@@ -58,18 +63,27 @@ impl JobThread {
|
||||
|
||||
pub fn interrupt_idle(&self, context: &Context) {
|
||||
{
|
||||
self.state.0.lock().unwrap().jobs_needed = 1;
|
||||
self.state.0.lock().unwrap().jobs_needed = true;
|
||||
}
|
||||
|
||||
info!(context, "Interrupting {}-IDLE...", self.name);
|
||||
|
||||
self.imap.interrupt_idle();
|
||||
// interrupt imap idle
|
||||
let &(ref lock, ref cvar) = &*self.watch.clone();
|
||||
{
|
||||
let mut watch = lock.lock().unwrap();
|
||||
|
||||
*watch = true;
|
||||
cvar.notify_one();
|
||||
}
|
||||
|
||||
let &(ref lock, ref cvar) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
state.idle = true;
|
||||
{
|
||||
let mut state = lock.lock().unwrap();
|
||||
state.idle = true;
|
||||
}
|
||||
cvar.notify_one();
|
||||
info!(context, "{}-idle interrupt done", self.name);
|
||||
}
|
||||
|
||||
pub fn fetch(&mut self, context: &Context, use_network: bool) {
|
||||
@@ -106,12 +120,12 @@ impl JobThread {
|
||||
self.state.0.lock().unwrap().using_handle = false;
|
||||
}
|
||||
|
||||
fn connect_to_imap(&self, context: &Context) -> bool {
|
||||
fn connect_to_imap(&mut self, context: &Context) -> bool {
|
||||
if self.imap.is_connected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut ret_connected = dc_connect_to_configured_imap(context, &self.imap) != 0;
|
||||
let mut ret_connected = dc_connect_to_configured_imap(context, &mut self.imap) != 0;
|
||||
|
||||
if ret_connected {
|
||||
if context
|
||||
@@ -134,18 +148,18 @@ impl JobThread {
|
||||
ret_connected
|
||||
}
|
||||
|
||||
pub fn idle(&self, context: &Context, use_network: bool) {
|
||||
pub fn idle(&mut self, context: &Context, use_network: bool) {
|
||||
{
|
||||
let &(ref lock, ref cvar) = &*self.state.clone();
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
if 0 != state.jobs_needed {
|
||||
if state.jobs_needed {
|
||||
info!(
|
||||
context,
|
||||
"{}-IDLE will not be started as it was interrupted while not ideling.",
|
||||
self.name,
|
||||
);
|
||||
state.jobs_needed = 0;
|
||||
state.jobs_needed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,14 +62,11 @@ impl Kml {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn parse(context: &Context, content: impl AsRef<str>) -> Result<Self, Error> {
|
||||
ensure!(
|
||||
content.as_ref().len() <= (1024 * 1024),
|
||||
"A kml-files with {} bytes is larger than reasonably expected.",
|
||||
content.as_ref().len()
|
||||
);
|
||||
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> {
|
||||
ensure!(content.len() <= 1024 * 1024, "kml-file is too large");
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(content.as_ref());
|
||||
let to_parse = String::from_utf8_lossy(content);
|
||||
let mut reader = quick_xml::Reader::from_str(&to_parse);
|
||||
reader.trim_text(true);
|
||||
|
||||
let mut kml = Kml::new();
|
||||
@@ -222,7 +219,7 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str =
|
||||
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
|
||||
chat::add_device_msg(context, chat_id, stock_str);
|
||||
chat::add_info_msg(context, chat_id, stock_str);
|
||||
}
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
if 0 != seconds {
|
||||
@@ -278,7 +275,7 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
1,
|
||||
DC_CONTACT_ID_SELF,
|
||||
]
|
||||
) {
|
||||
warn!(context, "failed to store location {:?}", err);
|
||||
@@ -369,9 +366,6 @@ pub fn delete_all(context: &Context) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> {
|
||||
let now = time();
|
||||
let mut location_count = 0;
|
||||
let mut ret = String::new();
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
let self_addr = context
|
||||
@@ -388,14 +382,17 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
|
||||
Ok((send_begin, send_until, last_sent))
|
||||
})?;
|
||||
|
||||
if !(locations_send_begin == 0 || now > locations_send_until) {
|
||||
let now = time();
|
||||
let mut location_count = 0;
|
||||
let mut ret = String::new();
|
||||
if locations_send_begin != 0 && now <= locations_send_until {
|
||||
ret += &format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
|
||||
self_addr,
|
||||
);
|
||||
|
||||
context.sql.query_map(
|
||||
"SELECT id, latitude, longitude, accuracy, timestamp\
|
||||
"SELECT id, latitude, longitude, accuracy, timestamp \
|
||||
FROM locations WHERE from_id=? \
|
||||
AND timestamp>=? \
|
||||
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
|
||||
@@ -416,7 +413,7 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
|
||||
for row in rows {
|
||||
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
|
||||
ret += &format!(
|
||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n\x00",
|
||||
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
|
||||
timestamp,
|
||||
accuracy,
|
||||
longitude,
|
||||
@@ -428,10 +425,10 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
|
||||
Ok(())
|
||||
}
|
||||
)?;
|
||||
ret += "</Document>\n</kml>";
|
||||
}
|
||||
|
||||
ensure!(location_count > 0, "No locations processed");
|
||||
ret += "</Document>\n</kml>";
|
||||
|
||||
Ok((ret, last_added_location_id))
|
||||
}
|
||||
@@ -495,7 +492,7 @@ pub fn save(
|
||||
chat_id: u32,
|
||||
contact_id: u32,
|
||||
locations: &[Location],
|
||||
independent: i32,
|
||||
independent: bool,
|
||||
) -> Result<u32, Error> {
|
||||
ensure!(chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id");
|
||||
context.sql.prepare2(
|
||||
@@ -510,7 +507,7 @@ pub fn save(
|
||||
for location in locations {
|
||||
let exists = stmt_test.exists(params![location.timestamp, contact_id as i32])?;
|
||||
|
||||
if 0 != independent || !exists {
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(params![
|
||||
location.timestamp,
|
||||
contact_id as i32,
|
||||
@@ -654,7 +651,7 @@ pub fn job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) {
|
||||
params![chat_id as i32],
|
||||
).is_ok() {
|
||||
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
|
||||
chat::add_device_msg(context, chat_id, stock_str);
|
||||
chat::add_info_msg(context, chat_id, stock_str);
|
||||
context.call_cb(Event::ChatModified(chat_id));
|
||||
}
|
||||
}
|
||||
@@ -672,9 +669,9 @@ mod tests {
|
||||
let context = dummy_context();
|
||||
|
||||
let xml =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
|
||||
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
|
||||
|
||||
let kml = Kml::parse(&context.ctx, &xml).expect("parsing failed");
|
||||
let kml = Kml::parse(&context.ctx, xml).expect("parsing failed");
|
||||
|
||||
assert!(kml.addr.is_some());
|
||||
assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);
|
||||
|
||||
@@ -476,8 +476,8 @@ impl Message {
|
||||
|
||||
pub fn is_info(&self) -> bool {
|
||||
let cmd = self.param.get_cmd();
|
||||
self.from_id == DC_CONTACT_ID_DEVICE as libc::c_uint
|
||||
|| self.to_id == DC_CONTACT_ID_DEVICE as libc::c_uint
|
||||
self.from_id == DC_CONTACT_ID_INFO as libc::c_uint
|
||||
|| self.to_id == DC_CONTACT_ID_INFO as libc::c_uint
|
||||
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
|
||||
ret += "\n";
|
||||
}
|
||||
|
||||
if msg.from_id == 2 || msg.to_id == 2 {
|
||||
if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO {
|
||||
// device-internal message, no further details needed
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -484,6 +484,7 @@ impl<'a> MimeFactory<'a> {
|
||||
if !meta_part.is_null() {
|
||||
mailmime_smart_add_part(message, meta_part);
|
||||
}
|
||||
|
||||
if self.msg.param.exists(Param::SetLatitude) {
|
||||
let param = &self.msg.param;
|
||||
let kml_file = location::get_message_kml(
|
||||
@@ -500,18 +501,21 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
|
||||
if let Ok((kml_file, last_added_location_id)) =
|
||||
location::get_kml(context, self.msg.chat_id)
|
||||
{
|
||||
wrapmime::add_filename_part(
|
||||
message,
|
||||
"location.kml",
|
||||
"application/vnd.google-earth.kml+xml",
|
||||
&kml_file,
|
||||
)?;
|
||||
if !self.msg.param.exists(Param::SetLatitude) {
|
||||
// otherwise, the independent location is already filed
|
||||
self.out_last_added_location_id = last_added_location_id;
|
||||
match location::get_kml(context, self.msg.chat_id) {
|
||||
Ok((kml_content, last_added_location_id)) => {
|
||||
wrapmime::add_filename_part(
|
||||
message,
|
||||
"location.kml",
|
||||
"application/vnd.google-earth.kml+xml",
|
||||
&kml_content,
|
||||
)?;
|
||||
if !self.msg.param.exists(Param::SetLatitude) {
|
||||
// otherwise, the independent location is already filed
|
||||
self.out_last_added_location_id = last_added_location_id;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "mimefactory: could not get location: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ pub enum Param {
|
||||
ProfileImage = b'i',
|
||||
// For Chats
|
||||
Selftalk = b'K',
|
||||
// For Chats
|
||||
Devicetalk = b'D',
|
||||
// For QR
|
||||
Auth = b's',
|
||||
// For QR
|
||||
|
||||
@@ -150,7 +150,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
|
||||
let (id, _) = chat::create_or_lookup_by_contact_id(context, lot.id, Blocked::Deaddrop)
|
||||
.unwrap_or_default();
|
||||
|
||||
chat::add_device_msg(
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
id,
|
||||
format!("{} verified.", peerstate.addr.unwrap_or_default()),
|
||||
|
||||
@@ -638,7 +638,7 @@ fn secure_connection_established(context: &Context, contact_chat_id: u32) {
|
||||
"?"
|
||||
};
|
||||
let msg = context.stock_string_repl_str(StockMessage::ContactVerified, addr);
|
||||
chat::add_device_msg(context, contact_chat_id, msg);
|
||||
chat::add_info_msg(context, contact_chat_id, msg);
|
||||
emit_event!(context, Event::ChatModified(contact_chat_id));
|
||||
}
|
||||
|
||||
@@ -654,7 +654,7 @@ fn could_not_establish_secure_connection(context: &Context, contact_chat_id: u32
|
||||
},
|
||||
);
|
||||
|
||||
chat::add_device_msg(context, contact_chat_id, &msg);
|
||||
chat::add_info_msg(context, contact_chat_id, &msg);
|
||||
error!(context, "{} ({})", &msg, details);
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<
|
||||
};
|
||||
let msg = context.stock_string_repl_str(StockMessage::ContactSetupChanged, peeraddr);
|
||||
|
||||
chat::add_device_msg(context, contact_chat_id, msg);
|
||||
chat::add_info_msg(context, contact_chat_id, msg);
|
||||
emit_event!(context, Event::ChatModified(contact_chat_id));
|
||||
}
|
||||
}
|
||||
|
||||
49
src/smtp.rs
49
src/smtp.rs
@@ -44,27 +44,24 @@ impl Smtp {
|
||||
}
|
||||
|
||||
/// Connect using the provided login params
|
||||
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool {
|
||||
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<(), Error> {
|
||||
if self.is_connected() {
|
||||
warn!(context, "SMTP already connected.");
|
||||
return true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if lp.send_server.is_empty() || lp.send_port == 0 {
|
||||
context.call_cb(Event::ErrorNetwork("SMTP bad parameters.".into()));
|
||||
bail!("SMTP Bad parameters");
|
||||
}
|
||||
|
||||
self.from = if let Ok(addr) = EmailAddress::new(lp.addr.clone()) {
|
||||
Some(addr)
|
||||
} else {
|
||||
None
|
||||
self.from = match EmailAddress::new(lp.addr.clone()) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
bail!("invalid login address {}: {}", lp.addr, err);
|
||||
}
|
||||
};
|
||||
|
||||
if self.from.is_none() {
|
||||
// TODO: print error
|
||||
return false;
|
||||
}
|
||||
|
||||
let domain = &lp.send_server;
|
||||
let port = lp.send_port as u16;
|
||||
|
||||
@@ -76,11 +73,12 @@ impl Smtp {
|
||||
let addr = &lp.addr;
|
||||
let send_pw = &lp.send_pw;
|
||||
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false);
|
||||
if access_token.is_none() {
|
||||
return false;
|
||||
}
|
||||
ensure!(
|
||||
access_token.is_some(),
|
||||
"could not get oaut2_access token addr={}",
|
||||
addr
|
||||
);
|
||||
let user = &lp.send_user;
|
||||
|
||||
(
|
||||
lettre::smtp::authentication::Credentials::new(
|
||||
user.to_string(),
|
||||
@@ -125,27 +123,27 @@ impl Smtp {
|
||||
"SMTP-LOGIN as {} ok",
|
||||
lp.send_user,
|
||||
)));
|
||||
return true;
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP: failed to connect {:?}", err);
|
||||
bail!("SMTP: failed to connect {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP: failed to setup connection {:?}", err);
|
||||
bail!("SMTP: failed to setup connection {:?}", err);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// SMTP-Send a prepared mail to recipients.
|
||||
/// returns boolean whether send was successful.
|
||||
/// on successful send out Ok() is returned.
|
||||
pub fn send<'a>(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
recipients: Vec<EmailAddress>,
|
||||
message: Vec<u8>,
|
||||
job_id: u32,
|
||||
) -> Result<(), Error> {
|
||||
let message_len = message.len();
|
||||
|
||||
@@ -156,12 +154,15 @@ impl Smtp {
|
||||
.join(",");
|
||||
|
||||
if let Some(ref mut transport) = self.transport {
|
||||
let envelope = Envelope::new(self.from.clone(), recipients);
|
||||
ensure!(envelope.is_ok(), "internal smtp-message construction fail");
|
||||
let envelope = envelope.unwrap();
|
||||
let envelope = match Envelope::new(self.from.clone(), recipients) {
|
||||
Ok(env) => env,
|
||||
Err(err) => {
|
||||
bail!("{}", err);
|
||||
}
|
||||
};
|
||||
let mail = SendableEmail::new(
|
||||
envelope,
|
||||
"mail-id".into(), // TODO: random id
|
||||
format!("{}", job_id), // only used for internal logging
|
||||
message,
|
||||
);
|
||||
|
||||
|
||||
10
src/sql.rs
10
src/sql.rs
@@ -92,8 +92,7 @@ impl Sql {
|
||||
self.start_stmt(sql.to_string());
|
||||
self.with_conn(|conn| {
|
||||
let stmt = conn.prepare(sql)?;
|
||||
let res = g(stmt, conn)?;
|
||||
Ok(res)
|
||||
g(stmt, conn)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,8 +105,7 @@ impl Sql {
|
||||
let stmt1 = conn.prepare(sql1)?;
|
||||
let stmt2 = conn.prepare(sql2)?;
|
||||
|
||||
let res = g(stmt1, stmt2, conn)?;
|
||||
Ok(res)
|
||||
g(stmt1, stmt2, conn)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -385,8 +383,8 @@ fn open(
|
||||
)?;
|
||||
sql.execute(
|
||||
"INSERT INTO contacts (id,name,origin) VALUES \
|
||||
(1,'self',262144), (2,'device',262144), (3,'rsvd',262144), \
|
||||
(4,'rsvd',262144), (5,'rsvd',262144), (6,'rsvd',262144), \
|
||||
(1,'self',262144), (2,'info',262144), (3,'rsvd',262144), \
|
||||
(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144), \
|
||||
(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);",
|
||||
params![],
|
||||
)?;
|
||||
|
||||
@@ -110,6 +110,8 @@ pub enum StockMessage {
|
||||
Location = 66,
|
||||
#[strum(props(fallback = "Sticker"))]
|
||||
Sticker = 67,
|
||||
#[strum(props(fallback = "Device Messages"))]
|
||||
DeviceMessages = 68,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
|
||||
@@ -143,7 +143,8 @@ pub fn get_field_date(imffields: *mut mailimf_fields) -> Result<i64, Error> {
|
||||
|
||||
fn mailimf_get_recipients_add_addr(recipients: &mut HashSet<String>, mb: *mut mailimf_mailbox) {
|
||||
if !mb.is_null() {
|
||||
let addr_norm = addr_normalize(as_str(unsafe { (*mb).mb_addr_spec }));
|
||||
let addr = to_string_lossy(unsafe { (*mb).mb_addr_spec });
|
||||
let addr_norm = addr_normalize(&addr);
|
||||
recipients.insert(addr_norm.into());
|
||||
}
|
||||
}
|
||||
@@ -382,8 +383,8 @@ pub fn mailimf_find_first_addr(mb_list: *const mailimf_mailbox_list) -> Option<S
|
||||
for cur in unsafe { (*(*mb_list).mb_list).into_iter() } {
|
||||
let mb = cur as *mut mailimf_mailbox;
|
||||
if !mb.is_null() && !unsafe { (*mb).mb_addr_spec.is_null() } {
|
||||
let addr = unsafe { as_str((*mb).mb_addr_spec) };
|
||||
return Some(addr_normalize(addr).to_string());
|
||||
let addr = unsafe { to_string_lossy((*mb).mb_addr_spec) };
|
||||
return Some(addr_normalize(&addr).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +490,9 @@ pub fn content_type_needs_encoding(content: *const mailmime_content) -> bool {
|
||||
if (*(*content).ct_type).tp_type == MAILMIME_TYPE_COMPOSITE_TYPE as libc::c_int {
|
||||
let composite = (*(*content).ct_type).tp_data.tp_composite_type;
|
||||
match (*composite).ct_type as u32 {
|
||||
MAILMIME_COMPOSITE_TYPE_MESSAGE => as_str((*content).ct_subtype) != "rfc822",
|
||||
MAILMIME_COMPOSITE_TYPE_MESSAGE => {
|
||||
to_string_lossy((*content).ct_subtype) != "rfc822"
|
||||
}
|
||||
MAILMIME_COMPOSITE_TYPE_MULTIPART => false,
|
||||
_ => false,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user