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 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, pub rfc724_mid: *mut libc::c_char, pub in_reply_to: Option, pub server_folder: Option, 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 = 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::, _>>().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 { 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 { 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>(2)?; msg.server_folder = row.get::<_, Option>(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>(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 = 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>(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 { 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>, 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 { 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 = 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>) { 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, 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(); } }