mirror of
https://github.com/chatmail/core.git
synced 2026-04-20 15:06:30 +03:00
1273 lines
38 KiB
Rust
1273 lines
38 KiB
Rust
use std::ffi::CString;
|
||
use std::path::{Path, PathBuf};
|
||
use std::ptr;
|
||
|
||
use deltachat_derive::{FromSql, ToSql};
|
||
use phf::phf_map;
|
||
|
||
use crate::chat::{self, Chat};
|
||
use crate::constants::*;
|
||
use crate::contact::*;
|
||
use crate::context::*;
|
||
use crate::dc_tools::*;
|
||
use crate::error::Error;
|
||
use crate::events::Event;
|
||
use crate::job::*;
|
||
use crate::lot::{Lot, LotState, Meaning};
|
||
use crate::param::*;
|
||
use crate::pgp::*;
|
||
use crate::sql;
|
||
use crate::stock::StockMessage;
|
||
use crate::x::*;
|
||
|
||
/// In practice, the user additionally cuts the string himself pixel-accurate.
|
||
const SUMMARY_CHARACTERS: usize = 160;
|
||
|
||
#[repr(i32)]
|
||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
|
||
pub enum MessageState {
|
||
Undefined = 0,
|
||
InFresh = 10,
|
||
InNoticed = 13,
|
||
InSeen = 16,
|
||
OutPreparing = 18,
|
||
OutDraft = 19,
|
||
OutPending = 20,
|
||
OutFailed = 24,
|
||
OutDelivered = 26,
|
||
OutMdnRcvd = 28,
|
||
}
|
||
|
||
impl Default for MessageState {
|
||
fn default() -> Self {
|
||
MessageState::Undefined
|
||
}
|
||
}
|
||
|
||
impl From<MessageState> for LotState {
|
||
fn from(s: MessageState) -> Self {
|
||
use MessageState::*;
|
||
match s {
|
||
Undefined => LotState::Undefined,
|
||
InFresh => LotState::MsgInFresh,
|
||
InNoticed => LotState::MsgInNoticed,
|
||
InSeen => LotState::MsgInSeen,
|
||
OutPreparing => LotState::MsgOutPreparing,
|
||
OutDraft => LotState::MsgOutDraft,
|
||
OutPending => LotState::MsgOutPending,
|
||
OutFailed => LotState::MsgOutFailed,
|
||
OutDelivered => LotState::MsgOutDelivered,
|
||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl MessageState {
|
||
pub fn can_fail(self) -> bool {
|
||
match self {
|
||
MessageState::OutPreparing | MessageState::OutPending | MessageState::OutDelivered => {
|
||
true
|
||
}
|
||
_ => false,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Lot {
|
||
/* library-internal */
|
||
/* in practice, the user additionally cuts the string himself pixel-accurate */
|
||
pub fn fill(
|
||
&mut self,
|
||
msg: &mut Message,
|
||
chat: &Chat,
|
||
contact: Option<&Contact>,
|
||
context: &Context,
|
||
) {
|
||
if msg.state == MessageState::OutDraft {
|
||
self.text1 = Some(context.stock_str(StockMessage::Draft).to_owned().into());
|
||
self.text1_meaning = Meaning::Text1Draft;
|
||
} else if msg.from_id == DC_CONTACT_ID_SELF {
|
||
if dc_msg_is_info(msg) || chat.is_self_talk() {
|
||
self.text1 = None;
|
||
self.text1_meaning = Meaning::None;
|
||
} else {
|
||
self.text1 = Some(context.stock_str(StockMessage::SelfMsg).to_owned().into());
|
||
self.text1_meaning = Meaning::Text1Self;
|
||
}
|
||
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
|
||
if dc_msg_is_info(msg) || contact.is_none() {
|
||
self.text1 = None;
|
||
self.text1_meaning = Meaning::None;
|
||
} else {
|
||
if chat.id == DC_CHAT_ID_DEADDROP {
|
||
if let Some(contact) = contact {
|
||
self.text1 = Some(contact.get_display_name().into());
|
||
} else {
|
||
self.text1 = None;
|
||
}
|
||
} else {
|
||
if let Some(contact) = contact {
|
||
self.text1 = Some(contact.get_first_name().into());
|
||
} else {
|
||
self.text1 = None;
|
||
}
|
||
}
|
||
self.text1_meaning = Meaning::Text1Username;
|
||
}
|
||
}
|
||
|
||
self.text2 = Some(dc_msg_get_summarytext_by_raw(
|
||
msg.type_0,
|
||
msg.text.as_ref(),
|
||
&mut msg.param,
|
||
SUMMARY_CHARACTERS,
|
||
context,
|
||
));
|
||
|
||
self.timestamp = dc_msg_get_timestamp(msg);
|
||
self.state = msg.state.into();
|
||
}
|
||
}
|
||
|
||
/// An object representing a single message in memory.
|
||
/// The message object is not updated.
|
||
/// If you want an update, you have to recreate the object.
|
||
///
|
||
/// to check if a mail was sent, use dc_msg_is_sent()
|
||
/// approx. max. length returned by dc_msg_get_text()
|
||
/// approx. max. length returned by dc_get_msg_info()
|
||
#[derive(Debug, Clone)]
|
||
pub struct Message {
|
||
pub id: u32,
|
||
pub from_id: u32,
|
||
pub to_id: u32,
|
||
pub chat_id: u32,
|
||
pub move_state: MoveState,
|
||
pub type_0: Viewtype,
|
||
pub state: MessageState,
|
||
pub hidden: bool,
|
||
pub timestamp_sort: i64,
|
||
pub timestamp_sent: i64,
|
||
pub timestamp_rcvd: i64,
|
||
pub text: Option<String>,
|
||
pub rfc724_mid: *mut libc::c_char,
|
||
pub in_reply_to: Option<String>,
|
||
pub server_folder: Option<String>,
|
||
pub server_uid: u32,
|
||
// TODO: enum
|
||
pub is_dc_message: u32,
|
||
pub starred: bool,
|
||
pub chat_blocked: Blocked,
|
||
pub location_id: u32,
|
||
pub param: Params,
|
||
}
|
||
|
||
// handle messages
|
||
pub unsafe fn dc_get_msg_info(context: &Context, msg_id: u32) -> *mut libc::c_char {
|
||
let mut ret = String::new();
|
||
|
||
let msg = dc_msg_load_from_db(context, msg_id);
|
||
if msg.is_err() {
|
||
return ptr::null_mut();
|
||
}
|
||
|
||
let msg = msg.unwrap();
|
||
|
||
let rawtxt: Option<String> = context.sql.query_get_value(
|
||
context,
|
||
"SELECT txt_raw FROM msgs WHERE id=?;",
|
||
params![msg_id as i32],
|
||
);
|
||
|
||
if rawtxt.is_none() {
|
||
ret += &format!("Cannot load message #{}.", msg_id as usize);
|
||
return ret.strdup();
|
||
}
|
||
let rawtxt = rawtxt.unwrap();
|
||
let rawtxt = dc_truncate(rawtxt.trim(), 100000, false);
|
||
|
||
let fts = dc_timestamp_to_str(dc_msg_get_timestamp(&msg));
|
||
ret += &format!("Sent: {}", fts);
|
||
|
||
let name = Contact::load_from_db(context, msg.from_id)
|
||
.map(|contact| contact.get_name_n_addr())
|
||
.unwrap_or_default();
|
||
|
||
ret += &format!(" by {}", name);
|
||
ret += "\n";
|
||
|
||
if msg.from_id != DC_CONTACT_ID_SELF as libc::c_uint {
|
||
let s = dc_timestamp_to_str(if 0 != msg.timestamp_rcvd {
|
||
msg.timestamp_rcvd
|
||
} else {
|
||
msg.timestamp_sort
|
||
});
|
||
ret += &format!("Received: {}", &s);
|
||
ret += "\n";
|
||
}
|
||
|
||
if msg.from_id == 2 || msg.to_id == 2 {
|
||
// device-internal message, no further details needed
|
||
return ret.strdup();
|
||
}
|
||
|
||
if let Ok(rows) = context.sql.query_map(
|
||
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
|
||
params![msg_id as i32],
|
||
|row| {
|
||
let contact_id: i32 = row.get(0)?;
|
||
let ts: i64 = row.get(1)?;
|
||
Ok((contact_id, ts))
|
||
},
|
||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||
) {
|
||
for (contact_id, ts) in rows {
|
||
let fts = dc_timestamp_to_str(ts);
|
||
ret += &format!("Read: {}", fts);
|
||
|
||
let name = Contact::load_from_db(context, contact_id as u32)
|
||
.map(|contact| contact.get_name_n_addr())
|
||
.unwrap_or_default();
|
||
|
||
ret += &format!(" by {}", name);
|
||
ret += "\n";
|
||
}
|
||
}
|
||
|
||
ret += "State: ";
|
||
use MessageState::*;
|
||
match msg.state {
|
||
InFresh => ret += "Fresh",
|
||
InNoticed => ret += "Noticed",
|
||
InSeen => ret += "Seen",
|
||
OutDelivered => ret += "Delivered",
|
||
OutFailed => ret += "Failed",
|
||
OutMdnRcvd => ret += "Read",
|
||
OutPending => ret += "Pending",
|
||
OutPreparing => ret += "Preparing",
|
||
_ => ret += &format!("{}", msg.state),
|
||
}
|
||
|
||
if dc_msg_has_location(&msg) {
|
||
ret += ", Location sent";
|
||
}
|
||
|
||
let e2ee_errors = msg.param.get_int(Param::ErroneousE2ee).unwrap_or_default();
|
||
|
||
if 0 != e2ee_errors {
|
||
if 0 != e2ee_errors & 0x2 {
|
||
ret += ", Encrypted, no valid signature";
|
||
}
|
||
} else if 0 != msg.param.get_int(Param::GuranteeE2ee).unwrap_or_default() {
|
||
ret += ", Encrypted";
|
||
}
|
||
|
||
ret += "\n";
|
||
match msg.param.get(Param::Error) {
|
||
Some(err) => ret += &format!("Error: {}", err),
|
||
_ => {}
|
||
}
|
||
|
||
if let Some(path) = dc_msg_get_file(context, &msg) {
|
||
let bytes = dc_get_filebytes(context, &path);
|
||
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
|
||
}
|
||
|
||
if msg.type_0 != Viewtype::Text {
|
||
ret += "Type: ";
|
||
ret += &format!("{}", msg.type_0);
|
||
ret += "\n";
|
||
ret += &format!("Mimetype: {}\n", &dc_msg_get_filemime(&msg));
|
||
}
|
||
let w = msg.param.get_int(Param::Width).unwrap_or_default();
|
||
let h = msg.param.get_int(Param::Height).unwrap_or_default();
|
||
if w != 0 || h != 0 {
|
||
ret += &format!("Dimension: {} x {}\n", w, h,);
|
||
}
|
||
let duration = msg.param.get_int(Param::Duration).unwrap_or_default();
|
||
if duration != 0 {
|
||
ret += &format!("Duration: {} ms\n", duration,);
|
||
}
|
||
if !rawtxt.is_empty() {
|
||
ret += &format!("\n{}\n", rawtxt);
|
||
}
|
||
if !msg.rfc724_mid.is_null() && 0 != *msg.rfc724_mid.offset(0) as libc::c_int {
|
||
ret += &format!("\nMessage-ID: {}", as_str(msg.rfc724_mid));
|
||
}
|
||
if let Some(ref server_folder) = msg.server_folder {
|
||
if server_folder != "" {
|
||
ret += &format!("\nLast seen as: {}/{}", server_folder, msg.server_uid);
|
||
}
|
||
}
|
||
|
||
ret.strdup()
|
||
}
|
||
|
||
pub fn dc_msg_new_untyped() -> Message {
|
||
dc_msg_new(Viewtype::Unknown)
|
||
}
|
||
|
||
pub fn dc_msg_new(viewtype: Viewtype) -> Message {
|
||
Message {
|
||
id: 0,
|
||
from_id: 0,
|
||
to_id: 0,
|
||
chat_id: 0,
|
||
move_state: MoveState::Undefined,
|
||
type_0: viewtype,
|
||
state: MessageState::Undefined,
|
||
hidden: false,
|
||
timestamp_sort: 0,
|
||
timestamp_sent: 0,
|
||
timestamp_rcvd: 0,
|
||
text: None,
|
||
rfc724_mid: std::ptr::null_mut(),
|
||
in_reply_to: None,
|
||
server_folder: None,
|
||
server_uid: 0,
|
||
is_dc_message: 0,
|
||
starred: false,
|
||
chat_blocked: Blocked::Not,
|
||
location_id: 0,
|
||
param: Params::new(),
|
||
}
|
||
}
|
||
|
||
impl Drop for Message {
|
||
fn drop(&mut self) {
|
||
unsafe {
|
||
free(self.rfc724_mid.cast());
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn dc_msg_get_filemime(msg: &Message) -> String {
|
||
if let Some(m) = msg.param.get(Param::MimeType) {
|
||
return m.to_string();
|
||
} else if let Some(file) = msg.param.get(Param::File) {
|
||
if let Some((_, mime)) = dc_msg_guess_msgtype_from_suffix(Path::new(file)) {
|
||
return mime.to_string();
|
||
}
|
||
}
|
||
|
||
"application/octet-stream".to_string()
|
||
}
|
||
|
||
pub fn dc_msg_guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
|
||
static KNOWN: phf::Map<&'static str, (Viewtype, &'static str)> = phf_map! {
|
||
"mp3" => (Viewtype::Audio, "audio/mpeg"),
|
||
"aac" => (Viewtype::Audio, "audio/aac"),
|
||
"mp4" => (Viewtype::Video, "video/mp4"),
|
||
"jpg" => (Viewtype::Image, "image/jpeg"),
|
||
"jpeg" => (Viewtype::Image, "image/jpeg"),
|
||
"png" => (Viewtype::Image, "image/png"),
|
||
"webp" => (Viewtype::Image, "image/webp"),
|
||
"gif" => (Viewtype::Gif, "image/gif"),
|
||
"vcf" => (Viewtype::File, "text/vcard"),
|
||
"vcard" => (Viewtype::File, "text/vcard"),
|
||
};
|
||
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
|
||
|
||
KNOWN.get(extension).map(|x| *x)
|
||
}
|
||
|
||
pub unsafe fn dc_msg_get_file(context: &Context, msg: &Message) -> Option<PathBuf> {
|
||
msg.param
|
||
.get(Param::File)
|
||
.map(|f| dc_get_abs_path(context, f))
|
||
}
|
||
|
||
/**
|
||
* Check if a message has a location bound to it.
|
||
* These messages are also returned by dc_get_locations()
|
||
* and the UI may decide to display a special icon beside such messages,
|
||
*
|
||
* @memberof Message
|
||
* @param msg The message object.
|
||
* @return 1=Message has location bound to it, 0=No location bound to message.
|
||
*/
|
||
pub fn dc_msg_has_location(msg: &Message) -> bool {
|
||
msg.location_id != 0
|
||
}
|
||
|
||
/**
|
||
* Set any location that should be bound to the message object.
|
||
* The function is useful to add a marker to the map
|
||
* at a position different from the self-location.
|
||
* You should not call this function
|
||
* if you want to bind the current self-location to a message;
|
||
* this is done by dc_set_location() and dc_send_locations_to_chat().
|
||
*
|
||
* Typically results in the event #DC_EVENT_LOCATION_CHANGED with
|
||
* contact_id set to DC_CONTACT_ID_SELF.
|
||
*
|
||
* @memberof Message
|
||
* @param msg The message object.
|
||
* @param latitude North-south position of the location.
|
||
* @param longitude East-west position of the location.
|
||
* @return None.
|
||
*/
|
||
pub fn dc_msg_set_location(msg: &mut Message, latitude: libc::c_double, longitude: libc::c_double) {
|
||
if latitude == 0.0 && longitude == 0.0 {
|
||
return;
|
||
}
|
||
|
||
msg.param.set_float(Param::SetLatitude, latitude);
|
||
msg.param.set_float(Param::SetLongitude, longitude);
|
||
}
|
||
|
||
pub fn dc_msg_get_timestamp(msg: &Message) -> i64 {
|
||
if 0 != msg.timestamp_sent {
|
||
msg.timestamp_sent
|
||
} else {
|
||
msg.timestamp_sort
|
||
}
|
||
}
|
||
|
||
pub fn dc_msg_load_from_db(context: &Context, id: u32) -> Result<Message, Error> {
|
||
context.sql.query_row(
|
||
"SELECT \
|
||
m.id,rfc724_mid,m.mime_in_reply_to,m.server_folder,m.server_uid,m.move_state,m.chat_id, \
|
||
m.from_id,m.to_id,m.timestamp,m.timestamp_sent,m.timestamp_rcvd, m.type,m.state,m.msgrmsg,m.txt, \
|
||
m.param,m.starred,m.hidden,m.location_id, c.blocked \
|
||
FROM msgs m \
|
||
LEFT JOIN chats c ON c.id=m.chat_id WHERE m.id=?;",
|
||
params![id as i32],
|
||
|row| {
|
||
unsafe {
|
||
let mut msg = dc_msg_new_untyped();
|
||
msg.id = row.get::<_, i32>(0)? as u32;
|
||
msg.rfc724_mid = row.get::<_, String>(1)?.strdup();
|
||
msg.in_reply_to = row.get::<_, Option<String>>(2)?;
|
||
msg.server_folder = row.get::<_, Option<String>>(3)?;
|
||
msg.server_uid = row.get(4)?;
|
||
msg.move_state = row.get(5)?;
|
||
msg.chat_id = row.get(6)?;
|
||
msg.from_id = row.get(7)?;
|
||
msg.to_id = row.get(8)?;
|
||
msg.timestamp_sort = row.get(9)?;
|
||
msg.timestamp_sent = row.get(10)?;
|
||
msg.timestamp_rcvd = row.get(11)?;
|
||
msg.type_0 = row.get(12)?;
|
||
msg.state = row.get(13)?;
|
||
msg.is_dc_message = row.get(14)?;
|
||
|
||
let text;
|
||
if let rusqlite::types::ValueRef::Text(buf) = row.get_raw(15) {
|
||
if let Ok(t) = String::from_utf8(buf.to_vec()) {
|
||
text = t;
|
||
} else {
|
||
warn!(context, "dc_msg_load_from_db: could not get text column as non-lossy utf8 id {}", id);
|
||
text = String::from_utf8_lossy(buf).into_owned();
|
||
}
|
||
} else {
|
||
text = "".to_string();
|
||
}
|
||
msg.text = Some(text);
|
||
|
||
msg.param = row.get::<_, String>(16)?.parse().unwrap_or_default();
|
||
msg.starred = row.get(17)?;
|
||
msg.hidden = row.get(18)?;
|
||
msg.location_id = row.get(19)?;
|
||
msg.chat_blocked = row.get::<_, Option<Blocked>>(20)?.unwrap_or_default();
|
||
if msg.chat_blocked == Blocked::Deaddrop {
|
||
if let Some(ref text) = msg.text {
|
||
let ptr = text.strdup();
|
||
|
||
dc_truncate_n_unwrap_str(ptr, 256, 0);
|
||
|
||
msg.text = Some(to_string(ptr));
|
||
free(ptr.cast());
|
||
}
|
||
};
|
||
Ok(msg)
|
||
}
|
||
})
|
||
}
|
||
|
||
pub unsafe fn dc_get_mime_headers(context: &Context, msg_id: u32) -> *mut libc::c_char {
|
||
let headers: Option<String> = context.sql.query_get_value(
|
||
context,
|
||
"SELECT mime_headers FROM msgs WHERE id=?;",
|
||
params![msg_id as i32],
|
||
);
|
||
|
||
if let Some(headers) = headers {
|
||
let h = CString::yolo(headers);
|
||
dc_strdup_keep_null(h.as_ptr())
|
||
} else {
|
||
std::ptr::null_mut()
|
||
}
|
||
}
|
||
|
||
pub unsafe fn dc_delete_msgs(context: &Context, msg_ids: *const u32, msg_cnt: libc::c_int) {
|
||
if msg_ids.is_null() || msg_cnt <= 0i32 {
|
||
return;
|
||
}
|
||
let mut i: libc::c_int = 0i32;
|
||
while i < msg_cnt {
|
||
dc_update_msg_chat_id(context, *msg_ids.offset(i as isize), 3i32 as u32);
|
||
job_add(
|
||
context,
|
||
Action::DeleteMsgOnImap,
|
||
*msg_ids.offset(i as isize) as libc::c_int,
|
||
Params::new(),
|
||
0,
|
||
);
|
||
i += 1
|
||
}
|
||
|
||
if 0 != msg_cnt {
|
||
context.call_cb(Event::MsgsChanged {
|
||
chat_id: 0,
|
||
msg_id: 0,
|
||
});
|
||
job_kill_action(context, Action::Housekeeping);
|
||
job_add(context, Action::Housekeeping, 0, Params::new(), 10);
|
||
};
|
||
}
|
||
|
||
fn dc_update_msg_chat_id(context: &Context, msg_id: u32, chat_id: u32) -> bool {
|
||
sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"UPDATE msgs SET chat_id=? WHERE id=?;",
|
||
params![chat_id as i32, msg_id as i32],
|
||
)
|
||
.is_ok()
|
||
}
|
||
|
||
pub fn dc_markseen_msgs(context: &Context, msg_ids: *const u32, msg_cnt: usize) -> bool {
|
||
if msg_ids.is_null() || msg_cnt <= 0 {
|
||
return false;
|
||
}
|
||
let msgs = context.sql.prepare(
|
||
"SELECT m.state, c.blocked FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id WHERE m.id=? AND m.chat_id>9",
|
||
|mut stmt, _| {
|
||
let mut res = Vec::with_capacity(msg_cnt);
|
||
for i in 0..msg_cnt {
|
||
let id = unsafe { *msg_ids.offset(i as isize) };
|
||
let query_res = stmt.query_row(params![id as i32], |row| {
|
||
Ok((row.get::<_, MessageState>(0)?, row.get::<_, Option<Blocked>>(1)?.unwrap_or_default()))
|
||
});
|
||
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
|
||
continue;
|
||
}
|
||
let (state, blocked) = query_res?;
|
||
res.push((id, state, blocked));
|
||
}
|
||
Ok(res)
|
||
}
|
||
);
|
||
|
||
if msgs.is_err() {
|
||
warn!(context, "markseen_msgs failed: {:?}", msgs);
|
||
return false;
|
||
}
|
||
let mut send_event = false;
|
||
let msgs = msgs.unwrap();
|
||
|
||
for (id, curr_state, curr_blocked) in msgs.into_iter() {
|
||
if curr_blocked == Blocked::Not {
|
||
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
|
||
dc_update_msg_state(context, id, MessageState::InSeen);
|
||
info!(context, "Seen message #{}.", id);
|
||
|
||
job_add(
|
||
context,
|
||
Action::MarkseenMsgOnImap,
|
||
id as i32,
|
||
Params::new(),
|
||
0,
|
||
);
|
||
send_event = true;
|
||
}
|
||
} else if curr_state == MessageState::InFresh {
|
||
dc_update_msg_state(context, id, MessageState::InNoticed);
|
||
send_event = true;
|
||
}
|
||
}
|
||
|
||
if send_event {
|
||
context.call_cb(Event::MsgsChanged {
|
||
chat_id: 0,
|
||
msg_id: 0,
|
||
});
|
||
}
|
||
|
||
true
|
||
}
|
||
|
||
pub fn dc_update_msg_state(context: &Context, msg_id: u32, state: MessageState) -> bool {
|
||
sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"UPDATE msgs SET state=? WHERE id=?;",
|
||
params![state, msg_id as i32],
|
||
)
|
||
.is_ok()
|
||
}
|
||
|
||
pub fn dc_star_msgs(
|
||
context: &Context,
|
||
msg_ids: *const u32,
|
||
msg_cnt: libc::c_int,
|
||
star: libc::c_int,
|
||
) -> bool {
|
||
if msg_ids.is_null() || msg_cnt <= 0 || star != 0 && star != 1 {
|
||
return false;
|
||
}
|
||
context
|
||
.sql
|
||
.prepare("UPDATE msgs SET starred=? WHERE id=?;", |mut stmt, _| {
|
||
for i in 0..msg_cnt {
|
||
stmt.execute(params![star, unsafe { *msg_ids.offset(i as isize) as i32 }])?;
|
||
}
|
||
Ok(())
|
||
})
|
||
.is_ok()
|
||
}
|
||
|
||
pub fn dc_get_msg(context: &Context, msg_id: u32) -> Result<Message, Error> {
|
||
dc_msg_load_from_db(context, msg_id)
|
||
}
|
||
|
||
pub fn dc_msg_get_id(msg: &Message) -> u32 {
|
||
msg.id
|
||
}
|
||
|
||
pub fn dc_msg_get_from_id(msg: &Message) -> u32 {
|
||
msg.from_id
|
||
}
|
||
|
||
pub fn dc_msg_get_chat_id(msg: &Message) -> u32 {
|
||
if msg.chat_blocked != Blocked::Not {
|
||
1
|
||
} else {
|
||
msg.chat_id
|
||
}
|
||
}
|
||
|
||
pub fn dc_msg_get_viewtype(msg: &Message) -> Viewtype {
|
||
msg.type_0
|
||
}
|
||
|
||
pub fn dc_msg_get_state(msg: &Message) -> MessageState {
|
||
msg.state
|
||
}
|
||
|
||
pub fn dc_msg_get_received_timestamp(msg: &Message) -> i64 {
|
||
msg.timestamp_rcvd
|
||
}
|
||
|
||
pub fn dc_msg_get_sort_timestamp(msg: &Message) -> i64 {
|
||
msg.timestamp_sort
|
||
}
|
||
|
||
pub unsafe fn dc_msg_get_text(msg: &Message) -> *mut libc::c_char {
|
||
if let Some(ref text) = msg.text {
|
||
dc_truncate(text, 30000, false).strdup()
|
||
} else {
|
||
ptr::null_mut()
|
||
}
|
||
}
|
||
|
||
#[allow(non_snake_case)]
|
||
pub unsafe fn dc_msg_get_filename(msg: &Message) -> *mut libc::c_char {
|
||
let mut ret = ptr::null_mut();
|
||
|
||
if let Some(file) = msg.param.get(Param::File) {
|
||
ret = dc_get_filename(file);
|
||
}
|
||
if !ret.is_null() {
|
||
ret
|
||
} else {
|
||
dc_strdup(0 as *const libc::c_char)
|
||
}
|
||
}
|
||
|
||
pub fn dc_msg_get_filebytes(context: &Context, msg: &Message) -> u64 {
|
||
if let Some(file) = msg.param.get(Param::File) {
|
||
return dc_get_filebytes(context, &file);
|
||
}
|
||
0
|
||
}
|
||
|
||
pub fn dc_msg_get_width(msg: &Message) -> libc::c_int {
|
||
msg.param.get_int(Param::Width).unwrap_or_default()
|
||
}
|
||
|
||
pub fn dc_msg_get_height(msg: &Message) -> libc::c_int {
|
||
msg.param.get_int(Param::Height).unwrap_or_default()
|
||
}
|
||
|
||
pub fn dc_msg_get_duration(msg: &Message) -> libc::c_int {
|
||
msg.param.get_int(Param::Duration).unwrap_or_default()
|
||
}
|
||
|
||
pub fn dc_msg_get_showpadlock(msg: &Message) -> bool {
|
||
msg.param.get_int(Param::GuranteeE2ee).unwrap_or_default() != 0
|
||
}
|
||
|
||
pub fn dc_msg_get_summary(context: &Context, msg: &mut Message, chat: Option<&Chat>) -> Lot {
|
||
let mut ret = Lot::new();
|
||
|
||
let chat_loaded: Chat;
|
||
let chat = if let Some(chat) = chat {
|
||
chat
|
||
} else {
|
||
if let Ok(chat) = Chat::load_from_db(context, msg.chat_id) {
|
||
chat_loaded = chat;
|
||
&chat_loaded
|
||
} else {
|
||
return ret;
|
||
}
|
||
};
|
||
|
||
let contact = if msg.from_id != DC_CONTACT_ID_SELF as libc::c_uint
|
||
&& ((*chat).typ == Chattype::Group || (*chat).typ == Chattype::VerifiedGroup)
|
||
{
|
||
Contact::get_by_id(context, msg.from_id).ok()
|
||
} else {
|
||
None
|
||
};
|
||
|
||
ret.fill(msg, chat, contact.as_ref(), context);
|
||
|
||
ret
|
||
}
|
||
|
||
pub unsafe fn dc_msg_get_summarytext(
|
||
context: &Context,
|
||
msg: &mut Message,
|
||
approx_characters: usize,
|
||
) -> *mut libc::c_char {
|
||
dc_msg_get_summarytext_by_raw(
|
||
msg.type_0,
|
||
msg.text.as_ref(),
|
||
&mut msg.param,
|
||
approx_characters,
|
||
context,
|
||
)
|
||
.strdup()
|
||
}
|
||
|
||
/// Returns a summary test.
|
||
pub fn dc_msg_get_summarytext_by_raw(
|
||
viewtype: Viewtype,
|
||
text: Option<impl AsRef<str>>,
|
||
param: &mut Params,
|
||
approx_characters: usize,
|
||
context: &Context,
|
||
) -> String {
|
||
let mut append_text = true;
|
||
let prefix = match viewtype {
|
||
Viewtype::Image => context.stock_str(StockMessage::Image).into_owned(),
|
||
Viewtype::Gif => context.stock_str(StockMessage::Gif).into_owned(),
|
||
Viewtype::Video => context.stock_str(StockMessage::Video).into_owned(),
|
||
Viewtype::Voice => context.stock_str(StockMessage::VoiceMessage).into_owned(),
|
||
Viewtype::Audio | Viewtype::File => {
|
||
if param.get_int(Param::Cmd) == Some(6) {
|
||
append_text = false;
|
||
context
|
||
.stock_str(StockMessage::AcSetupMsgSubject)
|
||
.to_string()
|
||
} else {
|
||
let file_name: String = if let Some(file_path) = param.get(Param::File) {
|
||
if let Some(file_name) = Path::new(file_path).file_name() {
|
||
Some(file_name.to_string_lossy().into_owned())
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
}
|
||
.unwrap_or("ErrFileName".to_string());
|
||
|
||
let label = context.stock_str(if viewtype == Viewtype::Audio {
|
||
StockMessage::Audio
|
||
} else {
|
||
StockMessage::File
|
||
});
|
||
format!("{} – {}", label, file_name)
|
||
}
|
||
}
|
||
_ => {
|
||
if param.get_int(Param::Cmd) != Some(9) {
|
||
"".to_string()
|
||
} else {
|
||
append_text = false;
|
||
context.stock_str(StockMessage::Location).to_string()
|
||
}
|
||
}
|
||
};
|
||
|
||
if !append_text {
|
||
return prefix;
|
||
}
|
||
|
||
if let Some(text) = text {
|
||
if prefix.is_empty() {
|
||
dc_truncate(text.as_ref(), approx_characters, true).to_string()
|
||
} else {
|
||
let tmp = format!("{} – {}", prefix, text.as_ref());
|
||
dc_truncate(&tmp, approx_characters, true).to_string()
|
||
}
|
||
} else {
|
||
prefix
|
||
}
|
||
}
|
||
|
||
pub unsafe fn dc_msg_has_deviating_timestamp(msg: &Message) -> libc::c_int {
|
||
let cnv_to_local = dc_gm2local_offset();
|
||
let sort_timestamp = dc_msg_get_sort_timestamp(msg) as i64 + cnv_to_local;
|
||
let send_timestamp = dc_msg_get_timestamp(msg) as i64 + cnv_to_local;
|
||
|
||
(sort_timestamp / 86400 != send_timestamp / 86400) as libc::c_int
|
||
}
|
||
|
||
pub fn dc_msg_is_sent(msg: &Message) -> bool {
|
||
msg.state as i32 >= MessageState::OutDelivered as i32
|
||
}
|
||
|
||
pub fn dc_msg_is_starred(msg: &Message) -> bool {
|
||
msg.starred
|
||
}
|
||
|
||
pub fn dc_msg_is_forwarded(msg: &Message) -> bool {
|
||
0 != msg.param.get_int(Param::Forwarded).unwrap_or_default()
|
||
}
|
||
|
||
pub fn dc_msg_is_info(msg: &Message) -> bool {
|
||
let cmd = msg.param.get_int(Param::Cmd).unwrap_or_default();
|
||
msg.from_id == 2i32 as libc::c_uint
|
||
|| msg.to_id == 2i32 as libc::c_uint
|
||
|| 0 != cmd && cmd != 6i32
|
||
}
|
||
|
||
pub fn dc_msg_is_increation(msg: &Message) -> bool {
|
||
chat::msgtype_has_file(msg.type_0) && msg.state == MessageState::OutPreparing
|
||
}
|
||
|
||
pub fn dc_msg_is_setupmessage(msg: &Message) -> bool {
|
||
if msg.type_0 != Viewtype::File {
|
||
return false;
|
||
}
|
||
|
||
msg.param.get_int(Param::Cmd) == Some(6)
|
||
}
|
||
|
||
pub unsafe fn dc_msg_get_setupcodebegin(context: &Context, msg: &Message) -> *mut libc::c_char {
|
||
// just a pointer inside buf, MUST NOT be free()'d
|
||
let mut buf_headerline: *const libc::c_char = ptr::null();
|
||
// just a pointer inside buf, MUST NOT be free()'d
|
||
let mut buf_setupcodebegin: *const libc::c_char = ptr::null();
|
||
let mut ret: *mut libc::c_char = ptr::null_mut();
|
||
if dc_msg_is_setupmessage(msg) {
|
||
if let Some(filename) = dc_msg_get_file(context, msg) {
|
||
if let Some(mut buf) = dc_read_file_safe(context, filename) {
|
||
if dc_split_armored_data(
|
||
buf.as_mut_ptr().cast(),
|
||
&mut buf_headerline,
|
||
&mut buf_setupcodebegin,
|
||
ptr::null_mut(),
|
||
ptr::null_mut(),
|
||
) && strcmp(
|
||
buf_headerline,
|
||
b"-----BEGIN PGP MESSAGE-----\x00" as *const u8 as *const libc::c_char,
|
||
) == 0
|
||
&& !buf_setupcodebegin.is_null()
|
||
{
|
||
ret = dc_strdup(buf_setupcodebegin)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if !ret.is_null() {
|
||
ret
|
||
} else {
|
||
dc_strdup(0 as *const libc::c_char)
|
||
}
|
||
}
|
||
|
||
pub fn dc_msg_set_text(msg: &mut Message, text: *const libc::c_char) {
|
||
msg.text = if text.is_null() {
|
||
None
|
||
} else {
|
||
Some(to_string(text))
|
||
};
|
||
}
|
||
|
||
pub fn dc_msg_set_file(
|
||
msg: &mut Message,
|
||
file: *const libc::c_char,
|
||
filemime: *const libc::c_char,
|
||
) {
|
||
if !file.is_null() {
|
||
msg.param.set(Param::File, as_str(file));
|
||
}
|
||
if !filemime.is_null() {
|
||
msg.param.set(Param::MimeType, as_str(filemime));
|
||
}
|
||
}
|
||
|
||
pub fn dc_msg_set_dimension(msg: &mut Message, width: libc::c_int, height: libc::c_int) {
|
||
msg.param.set_int(Param::Width, width);
|
||
msg.param.set_int(Param::Height, height);
|
||
}
|
||
|
||
pub fn dc_msg_set_duration(msg: &mut Message, duration: libc::c_int) {
|
||
msg.param.set_int(Param::Duration, duration);
|
||
}
|
||
|
||
pub fn dc_msg_latefiling_mediasize(
|
||
context: &Context,
|
||
msg: &mut Message,
|
||
width: libc::c_int,
|
||
height: libc::c_int,
|
||
duration: libc::c_int,
|
||
) {
|
||
if width > 0 && height > 0 {
|
||
msg.param.set_int(Param::Width, width);
|
||
msg.param.set_int(Param::Height, height);
|
||
}
|
||
if duration > 0 {
|
||
msg.param.set_int(Param::Duration, duration);
|
||
}
|
||
dc_msg_save_param_to_disk(context, msg);
|
||
}
|
||
|
||
pub fn dc_msg_save_param_to_disk(context: &Context, msg: &mut Message) -> bool {
|
||
sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"UPDATE msgs SET param=? WHERE id=?;",
|
||
params![msg.param.to_string(), msg.id as i32],
|
||
)
|
||
.is_ok()
|
||
}
|
||
|
||
pub fn dc_msg_new_load(context: &Context, msg_id: u32) -> Result<Message, Error> {
|
||
dc_msg_load_from_db(context, msg_id)
|
||
}
|
||
|
||
pub fn dc_delete_msg_from_db(context: &Context, msg_id: u32) {
|
||
if let Ok(msg) = dc_msg_load_from_db(context, msg_id) {
|
||
sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"DELETE FROM msgs WHERE id=?;",
|
||
params![msg.id as i32],
|
||
)
|
||
.ok();
|
||
sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"DELETE FROM msgs_mdns WHERE msg_id=?;",
|
||
params![msg.id as i32],
|
||
)
|
||
.ok();
|
||
}
|
||
}
|
||
|
||
/* as we do not cut inside words, this results in about 32-42 characters.
|
||
Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise.
|
||
It should also be very clear, the subject is _not_ the whole message.
|
||
The value is also used for CC:-summaries */
|
||
|
||
// Context functions to work with messages
|
||
|
||
pub fn dc_msg_exists(context: &Context, msg_id: u32) -> bool {
|
||
if msg_id <= DC_CHAT_ID_LAST_SPECIAL {
|
||
return false;
|
||
}
|
||
|
||
let chat_id: Option<u32> = context.sql.query_get_value(
|
||
context,
|
||
"SELECT chat_id FROM msgs WHERE id=?;",
|
||
params![msg_id],
|
||
);
|
||
|
||
if let Some(chat_id) = chat_id {
|
||
chat_id != DC_CHAT_ID_TRASH
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
pub fn dc_update_msg_move_state(
|
||
context: &Context,
|
||
rfc724_mid: *const libc::c_char,
|
||
state: MoveState,
|
||
) -> bool {
|
||
// we update the move_state for all messages belonging to a given Message-ID
|
||
// so that the state stay intact when parts are deleted
|
||
sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"UPDATE msgs SET move_state=? WHERE rfc724_mid=?;",
|
||
params![state as i32, as_str(rfc724_mid)],
|
||
)
|
||
.is_ok()
|
||
}
|
||
|
||
pub fn dc_set_msg_failed(context: &Context, msg_id: u32, error: Option<impl AsRef<str>>) {
|
||
if let Ok(mut msg) = dc_msg_load_from_db(context, msg_id) {
|
||
if msg.state.can_fail() {
|
||
msg.state = MessageState::OutFailed;
|
||
}
|
||
if let Some(error) = error {
|
||
msg.param.set(Param::Error, error.as_ref());
|
||
error!(context, "{}", error.as_ref());
|
||
}
|
||
|
||
if sql::execute(
|
||
context,
|
||
&context.sql,
|
||
"UPDATE msgs SET state=?, param=? WHERE id=?;",
|
||
params![msg.state, msg.param.to_string(), msg_id as i32],
|
||
)
|
||
.is_ok()
|
||
{
|
||
context.call_cb(Event::MsgFailed {
|
||
chat_id: msg.chat_id,
|
||
msg_id,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/* returns 1 if an event should be send */
|
||
pub unsafe fn dc_mdn_from_ext(
|
||
context: &Context,
|
||
from_id: u32,
|
||
rfc724_mid: *const libc::c_char,
|
||
timestamp_sent: i64,
|
||
ret_chat_id: *mut u32,
|
||
ret_msg_id: *mut u32,
|
||
) -> libc::c_int {
|
||
if from_id <= 9
|
||
|| rfc724_mid.is_null()
|
||
|| ret_chat_id.is_null()
|
||
|| ret_msg_id.is_null()
|
||
|| *ret_chat_id != 0
|
||
|| *ret_msg_id != 0
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
let mut read_by_all = 0;
|
||
|
||
if let Ok((msg_id, chat_id, chat_type, msg_state)) = context.sql.query_row(
|
||
"SELECT m.id, c.id, c.type, m.state FROM msgs m \
|
||
LEFT JOIN chats c ON m.chat_id=c.id \
|
||
WHERE rfc724_mid=? AND from_id=1 \
|
||
ORDER BY m.id;",
|
||
params![as_str(rfc724_mid)],
|
||
|row| {
|
||
Ok((
|
||
row.get::<_, i32>(0)?,
|
||
row.get::<_, i32>(1)?,
|
||
row.get::<_, Chattype>(2)?,
|
||
row.get::<_, MessageState>(3)?,
|
||
))
|
||
},
|
||
) {
|
||
*ret_msg_id = msg_id as u32;
|
||
*ret_chat_id = chat_id as u32;
|
||
|
||
/* if already marked as MDNS_RCVD msgstate_can_fail() returns false.
|
||
however, it is important, that ret_msg_id is set above as this
|
||
will allow the caller eg. to move the message away */
|
||
if msg_state.can_fail() {
|
||
let mdn_already_in_table = context
|
||
.sql
|
||
.exists(
|
||
"SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
|
||
params![*ret_msg_id as i32, from_id as i32,],
|
||
)
|
||
.unwrap_or_default();
|
||
|
||
if !mdn_already_in_table {
|
||
context.sql.execute(
|
||
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
|
||
params![*ret_msg_id as i32, from_id as i32, timestamp_sent],
|
||
).unwrap(); // TODO: better error handling
|
||
}
|
||
|
||
// Normal chat? that's quite easy.
|
||
if chat_type == Chattype::Single {
|
||
dc_update_msg_state(context, *ret_msg_id, MessageState::OutMdnRcvd);
|
||
read_by_all = 1;
|
||
} else {
|
||
/* send event about new state */
|
||
let ist_cnt: i32 = context
|
||
.sql
|
||
.query_get_value(
|
||
context,
|
||
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
|
||
params![*ret_msg_id as i32],
|
||
)
|
||
.unwrap_or_default();
|
||
/*
|
||
Groupsize: Min. MDNs
|
||
|
||
1 S n/a
|
||
2 SR 1
|
||
3 SRR 2
|
||
4 SRRR 2
|
||
5 SRRRR 3
|
||
6 SRRRRR 3
|
||
|
||
(S=Sender, R=Recipient)
|
||
*/
|
||
// for rounding, SELF is already included!
|
||
let soll_cnt = (chat::get_chat_contact_cnt(context, *ret_chat_id) + 1) / 2;
|
||
if ist_cnt >= soll_cnt {
|
||
dc_update_msg_state(context, *ret_msg_id, MessageState::OutMdnRcvd);
|
||
read_by_all = 1;
|
||
} /* else wait for more receipts */
|
||
}
|
||
}
|
||
}
|
||
|
||
read_by_all
|
||
}
|
||
|
||
/* the number of messages assigned to real chat (!=deaddrop, !=trash) */
|
||
pub fn dc_get_real_msg_cnt(context: &Context) -> libc::c_int {
|
||
match context.sql.query_row(
|
||
"SELECT COUNT(*) \
|
||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
|
||
WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
|
||
rusqlite::NO_PARAMS,
|
||
|row| row.get(0),
|
||
) {
|
||
Ok(res) => res,
|
||
Err(err) => {
|
||
error!(context, "dc_get_real_msg_cnt() failed. {}", err);
|
||
0
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn dc_get_deaddrop_msg_cnt(context: &Context) -> libc::size_t {
|
||
match context.sql.query_row(
|
||
"SELECT COUNT(*) \
|
||
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
|
||
WHERE c.blocked=2;",
|
||
rusqlite::NO_PARAMS,
|
||
|row| row.get::<_, isize>(0),
|
||
) {
|
||
Ok(res) => res as libc::size_t,
|
||
Err(err) => {
|
||
error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err);
|
||
0
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn dc_rfc724_mid_cnt(context: &Context, rfc724_mid: *const libc::c_char) -> libc::c_int {
|
||
/* check the number of messages with the same rfc724_mid */
|
||
match context.sql.query_row(
|
||
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;",
|
||
&[as_str(rfc724_mid)],
|
||
|row| row.get(0),
|
||
) {
|
||
Ok(res) => res,
|
||
Err(err) => {
|
||
error!(context, "dc_get_rfc724_mid_cnt() failed. {}", err);
|
||
0
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn dc_rfc724_mid_exists(
|
||
context: &Context,
|
||
rfc724_mid: *const libc::c_char,
|
||
ret_server_folder: *mut *mut libc::c_char,
|
||
ret_server_uid: *mut u32,
|
||
) -> u32 {
|
||
if rfc724_mid.is_null() || unsafe { *rfc724_mid.offset(0) as libc::c_int } == 0 {
|
||
return 0;
|
||
}
|
||
match context.sql.query_row(
|
||
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
|
||
&[as_str(rfc724_mid)],
|
||
|row| {
|
||
if !ret_server_folder.is_null() {
|
||
unsafe { *ret_server_folder = row.get::<_, String>(0)?.strdup() };
|
||
}
|
||
if !ret_server_uid.is_null() {
|
||
unsafe { *ret_server_uid = row.get(1)? };
|
||
}
|
||
row.get(2)
|
||
},
|
||
) {
|
||
Ok(res) => res,
|
||
Err(_err) => {
|
||
if !ret_server_folder.is_null() {
|
||
unsafe { *ret_server_folder = ptr::null_mut() };
|
||
}
|
||
if !ret_server_uid.is_null() {
|
||
unsafe { *ret_server_uid = 0 };
|
||
}
|
||
|
||
0
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn dc_update_server_uid(
|
||
context: &Context,
|
||
rfc724_mid: *const libc::c_char,
|
||
server_folder: impl AsRef<str>,
|
||
server_uid: u32,
|
||
) {
|
||
match context.sql.execute(
|
||
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;",
|
||
params![server_folder.as_ref(), server_uid, as_str(rfc724_mid)],
|
||
) {
|
||
Ok(_) => {}
|
||
Err(err) => {
|
||
warn!(context, "msg: failed to update server_uid: {}", err);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::test_utils as test;
|
||
|
||
#[test]
|
||
fn test_dc_msg_guess_msgtype_from_suffix() {
|
||
assert_eq!(
|
||
dc_msg_guess_msgtype_from_suffix(Path::new("foo/bar-sth.mp3")),
|
||
Some((Viewtype::Audio, "audio/mpeg"))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
pub fn test_prepare_message_and_send() {
|
||
use crate::config::Config;
|
||
|
||
let d = test::dummy_context();
|
||
let ctx = &d.ctx;
|
||
|
||
let contact =
|
||
Contact::create(ctx, "", "dest@example.com").expect("failed to create contact");
|
||
|
||
let res = ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"));
|
||
assert!(res.is_ok());
|
||
|
||
let chat = chat::create_by_contact_id(ctx, contact).unwrap();
|
||
|
||
let mut msg = dc_msg_new(Viewtype::Text);
|
||
|
||
let msg_id = chat::prepare_msg(ctx, chat, &mut msg).unwrap();
|
||
|
||
let _msg2 = dc_get_msg(ctx, msg_id).unwrap();
|
||
}
|
||
}
|