diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index c919d660d..53d42bbf4 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -11,23 +11,112 @@ extern crate human_panic; extern crate num_traits; -use num_traits::{FromPrimitive, ToPrimitive}; use std::convert::TryInto; +use std::ffi::CString; use std::ptr; use std::str::FromStr; +use libc::uintptr_t; +use num_traits::{FromPrimitive, ToPrimitive}; + +use deltachat::config::Config; +use deltachat::constants::Event; use deltachat::contact::Contact; -use deltachat::dc_tools::{as_str, StrExt}; +use deltachat::context::Context; +use deltachat::dc_tools::{as_path, as_str, to_string, Strdup}; use deltachat::*; // TODO: constants -// dc_context_t +/// The FFI callback type that should be passed to [dc_context_new]. +/// +/// @memberof Context +/// +/// # Parameters +/// +/// The callback should accept the following arguments: +/// +/// * `context` - The context object as returned by [dc_context_new]. +/// * `event` - one of the @ref DC_EVENT constants. +/// * `data1` - depends on the event parameter. +/// * `data2` - depends on the event parameter. +/// +/// # Returns +/// +/// This callback should return 0 unless stated otherwise in the event +/// parameter documentation. +pub type dc_callback_t = + unsafe extern "C" fn(_: &ContextWrapper, _: Event, _: uintptr_t, _: uintptr_t) -> uintptr_t; -pub type dc_context_t = context::Context; +pub struct ContextWrapper { + cb: Option, + userdata: *mut libc::c_void, + os_name: *const libc::c_char, + inner: Option, // TODO: Wrap this in RwLock +} -pub type dc_callback_t = types::dc_callback_t; +/// The FFI context type. +pub type dc_context_t = ContextWrapper; +impl ContextWrapper { + fn error(&self, msg: &str) { + if let Some(cb) = self.cb { + let msg_c = + CString::new(msg).unwrap_or(CString::new("[invalid error message]").unwrap()); + unsafe { cb(self, Event::ERROR, 0, msg_c.as_ptr() as libc::uintptr_t) }; + } + } +} + +/// Create a new context object. +/// +/// After creation it is usually opened, connected and mails are +/// fetched. +/// +/// @memberof [dc_context_t] +/// +/// # Parameters +/// +/// * `cb` - a callback function that is called for events (update, +/// state changes etc.) and to get some information from the client +/// (eg. translation for a given string). +/// +/// See @ref DC_EVENT for a list of possible events that may be passed to the callback. +/// +/// - The callback MAY be called from _any_ thread, not only the +/// main/GUI thread! +/// - The callback MUST NOT call any dc_* and related functions +/// unless stated otherwise! +/// - The callback SHOULD return _fast_, for GUI updates etc. you +/// should post yourself an asynchronous message to your GUI +/// thread, if needed. +/// - If not mentioned otherweise, the callback should return 0. +/// +/// This must have the same lifetime as the context otherwise events +/// will call garbage. +/// +/// * `userdata` - can be used by the client for any purpuse. Can be +/// retrieved using [dc_get_userdata]. This is never freed by +/// deltachat itself, even after the context is destroyed. It is +/// assumed this has the same lifetime as the context itself, +/// otherwise [dc_get_userdata] will return a pointer to freed +/// memory. +/// +/// * `os_name` - is only for decorative use and is shown eg. in the +/// `X-Mailer:` header in the form "Delta Chat Core +/// /". You can give the name of the app, the +/// operating system, the used environment and/or the version here. +/// It is okay to give NULL, in this case `X-Mailer:` header is set +/// to "Delta Chat Core ". +/// +/// This is never freed by deltachat itself. It can be destroyed as +/// soon as this function returns. +/// +/// # Returns +/// +/// A context object with some public members. The object must be +/// passed to the other context functions and must be freed using +/// [dc_context_unref] after usage. #[no_mangle] pub unsafe extern "C" fn dc_context_new( cb: Option, @@ -35,36 +124,71 @@ pub unsafe extern "C" fn dc_context_new( os_name: *const libc::c_char, ) -> *mut dc_context_t { setup_panic!(); - - let os_name = if os_name.is_null() { - None - } else { - Some(dc_tools::to_string_lossy(os_name)) + let wrapper = ContextWrapper { + cb, + userdata, + os_name: dc_tools::dc_strdup_keep_null(os_name), + inner: None, }; - let ctx = context::dc_context_new(cb, userdata, os_name); - - Box::into_raw(Box::new(ctx)) + Box::into_raw(Box::new(wrapper)) } -/// Release the context structure. +/// Free a context object. /// -/// This function releases the memory of the `dc_context_t` structure. +/// If app runs can only be terminated by a forced kill, this may be superfluous. +/// Before the context object is freed, connections to SMTP, IMAP and database +/// are closed. You can also do this explicitly by calling [dc_close] on your own +/// before calling [dc_context_unref]. +/// +/// @memberof dc_context_t +/// +/// # Parameters +/// +/// * `context` - The context object as created by [dc_context_new]. +/// If `NULL` is given, nothing is done. #[no_mangle] pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) { - assert!(!context.is_null()); - let context = &mut *context; - context::dc_close(context); - Box::from_raw(context); + if !context.is_null() { + let wrapper: &mut ContextWrapper = &mut *context; + Box::from_raw(wrapper); // Drops the wrapper and contained context + } } +/// Get user data associated with a context object. +/// +/// @memberof [dc_context_t] +/// +/// # Parameters +/// +/// * `context` - The context object as created by [dc_context_new]. +/// +/// # Returns +/// +/// The user data is returned, this is the second parameter given to +/// [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_get_userdata(context: *mut dc_context_t) -> *mut libc::c_void { assert!(!context.is_null()); - let context = &mut *context; - - context::dc_get_userdata(context) + let wrapper = &mut *context; + wrapper.userdata } +/// Open context database. +/// +/// If the given file does not exist, it is created and can be set up +/// using [dc_set_config] afterwards. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param dbfile The file to use to store the database, something like `~/file` won't +/// work on all systems, if in doubt, use absolute paths. +/// @param blobdir A directory to store the blobs in; a trailing slash is not needed. +/// If you pass NULL or the empty string, deltachat-core creates a directory +/// beside _dbfile_ with the same name and the suffix `-blobs`. +/// +/// Returns `1` on success, `0` on failure eg. if the file is not +/// writable or if there is already a database opened for the context. #[no_mangle] pub unsafe extern "C" fn dc_open( context: *mut dc_context_t, @@ -73,39 +197,161 @@ pub unsafe extern "C" fn dc_open( ) -> libc::c_int { assert!(!context.is_null()); assert!(!dbfile.is_null()); - let context = &mut *context; - - let dbfile_str = dc_tools::as_str(dbfile); - let blobdir_str = if blobdir.is_null() { - None - } else { - Some(dc_tools::as_str(blobdir)) + let rust_cb = move |_ctx: &Context, evt: Event, d0: uintptr_t, d1: uintptr_t| { + let wrapper: &ContextWrapper = &*context; + match wrapper.cb { + Some(ffi_cb) => ffi_cb(wrapper, evt, d0, d1), + None => 0, + } }; - context::dc_open(context, dbfile_str, blobdir_str) as libc::c_int + let mut wrapper: &mut ContextWrapper = &mut *context; + let new_context = if blobdir.is_null() { + Context::new( + Box::new(rust_cb), + to_string(wrapper.os_name), + as_path(dbfile).to_path_buf(), + ) + } else { + Context::with_blobdir( + Box::new(rust_cb), + to_string(wrapper.os_name), + as_path(dbfile).to_path_buf(), + as_path(blobdir), + ) + }; + match new_context { + Ok(mut ctx) => { + // Some structs, e.g. Chatlist, have a reference to the + // Context and allow to retrieve the context again. + // Because on the C API we need to return the + // ContextWrapper rather than the context we store the + // wrapper as userdata on the Context. The actual + // userdata exposed by the C API is stored on the + // ContextWrapper. + let wrapper_ptr: *mut ContextWrapper = wrapper; + ctx.userdata = wrapper_ptr as *mut libc::c_void; + wrapper.inner = Some(ctx); + 1 + } + Err(_) => 0, + } } +/// Close the context. +/// +/// This will disconnect the IMAP and SMTP connections and free some +/// structures. The context will still be using some memory until +/// [dc_context_unref] is called. +/// +/// This method is **not** thread safe. + +/// Close context database opened by [dc_open]. +/// +/// Before this, connections to SMTP and IMAP are closed; these +/// connections are started automatically as needed eg. by sending for +/// fetching messages. This function is also implicitly called by +/// [dc_context_unref]. Multiple calls to this functions are okay, +/// the function takes care not to free objects twice. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_close(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &mut *context; - context::dc_close(context) + let wrapper: &mut ContextWrapper = &mut *context; + if let Some(ref ctx) = &wrapper.inner { + context::dc_close(ctx); + wrapper.inner = None; + } } +/// Checks if the context is open. +/// +/// Returns `0` if the context is open, `1` if not. +/// +/// Note that this is inherently race-condition prone. #[no_mangle] pub unsafe extern "C" fn dc_is_open(context: *mut dc_context_t) -> libc::c_int { assert!(!context.is_null()); - let context = &mut *context; - context::dc_is_open(context) + let wrapper: &mut ContextWrapper = &mut *context; + match wrapper.inner { + Some(_) => 0, + None => 1, + } } +/// Get the blob directory. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// +/// Returns blob directory associated with the context object, empty +/// string if unset or on errors. NULL is never returned. The +/// returned string must be free()'d. #[no_mangle] pub unsafe extern "C" fn dc_get_blobdir(context: *mut dc_context_t) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); context::dc_get_blobdir(context) } +/// Sets a configuration option. +/// +/// The configuration is handled by key=value pairs as: +/// +/// * `addr` - address to display (always needed) +/// * `mail_server` - IMAP-server, guessed if left out +/// * `mail_user` - IMAP-username, guessed if left out +/// * `mail_pw` - IMAP-password (always needed) +/// * `mail_port` - IMAP-port, guessed if left out +/// * `send_server` - SMTP-server, guessed if left out +/// * `send_user` - SMTP-user, guessed if left out +/// * `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 +/// * `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. +/// NULL to remove the avatar. +/// It is planned for future versions +/// to send this image together with the next messages. +/// * `e2ee_enabled` - 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default) +/// * `mdns_enabled` - 0=do not send or request read receipts, +/// 1=send and request read receipts (default) +/// * `inbox_watch` - 1=watch `INBOX`-folder for changes (default), +/// 0=do not watch the `INBOX`-folder +/// * `sentbox_watch`- 1=watch `Sent`-folder for changes (default), +/// 0=do not watch the `Sent`-folder +/// * `mvbox_watch` - 1=watch `DeltaChat`-folder for changes (default), +/// 0=do not watch the `DeltaChat`-folder +/// * `mvbox_move` - 1=heuristically detect chat-messages +/// and move them to the `DeltaChat`-folder, +/// 0=do not move chat-messages +/// * `show_emails` - DC_SHOW_EMAILS_OFF (0)= +/// show direct replies to chats only (default), +/// DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)= +/// also show all mails of confirmed contacts, +/// DC_SHOW_EMAILS_ALL (2)= +/// also show mails of unconfirmed contacts in the deaddrop. +/// * `save_mime_headers` - 1=save mime headers +/// and make dc_get_mime_headers() work for subsequent calls, +/// 0=do not save mime headers (default) +/// +/// If you want to retrieve a value, use [dc_get_config]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object +/// @param key The option to change, see above. +/// @param value The value to save for "key" +/// +/// Returns `0` for failure and `1` for success. #[no_mangle] pub unsafe extern "C" fn dc_set_config( context: *mut dc_context_t, @@ -114,38 +360,111 @@ pub unsafe extern "C" fn dc_set_config( ) -> libc::c_int { assert!(!context.is_null()); assert!(!key.is_null(), "invalid key"); - let context = &*context; - - match config::Config::from_str(dc_tools::as_str(key)) { - Ok(key) => context.set_config(key, as_opt_str(value)).is_ok() as libc::c_int, - Err(_) => 0, + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); + match Config::from_str(as_str(key)) { + // context.set_config() did already log (TODO, it shouldn't) + Ok(key) => context + .set_config(key, as_opt_str(value)) + .and(Ok(1)) + .or(Err(0)) + .unwrap(), + Err(_) => { + wrapper.error("dc_set_config(): invalid key"); + 0 + } } } +/// Gets a configuration option. +/// +/// The configuration option is set by dc_set_config() or by the library itself. +/// +/// Beside the options shown at dc_set_config(), this function can be +/// used to query some global system values: +/// +/// * `sys.version` - get the version string eg. as `1.2.3` or as +/// `1.2.3special4` +/// * `sys.msgsize_max_recommended` - maximal recommended attachment +/// size in bytes. All possible overheads are +/// already subtracted and this value can be used +/// eg. for direct comparison with the size of a +/// file the user wants to attach. If an attachment +/// is larger than this value, an error (no warning +/// as it should be shown to the user) is logged +/// but the attachment is sent anyway. +/// * `sys.config_keys` - get a space-separated list of all +/// config-keys available. The config-keys are the +/// keys that can be passed to the parameter `key` +/// of this function. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by +/// dc_context_new(). For querying system values, this can be NULL. +/// @param key The key to query. +/// +/// Returns current value of "key", if "key" is unset, the default value is returned. +/// The returned value must be free()'d, NULL is never returned. #[no_mangle] pub unsafe extern "C" fn dc_get_config( context: *mut dc_context_t, key: *mut libc::c_char, ) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - assert!(!key.is_null(), "invalid key pointer"); - let key = config::Config::from_str(dc_tools::as_str(key)).expect("invalid key"); - - // TODO: Translating None to NULL would be more sensible than translating None - // to "", as it is now. + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); + let key = Config::from_str(as_str(key)).expect("invalid key"); context.get_config(key).unwrap_or_default().strdup() } +/// Gets information about the context. +/// +/// The information is returned as a multi-line string and contains +/// information about the current configuration. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by dc_context_new(). +/// +/// Returns a string which must be free()'d after usage. Never +/// returns NULL. #[no_mangle] pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); context::dc_get_info(context) } +/// Gets url that can be used to initiate an OAuth2 authorisation. +/// +/// If an OAuth2 authorization is possible for a given e-mail-address, +/// this function returns the URL that should be opened in a browser. +/// +/// If the user authorizes access, the given redirect_uri is called by +/// the provider. It's up to the UI to handle this call. +/// +/// The provider will attach some parameters to the url, most +/// important the parameter `code` that should be set as the +/// `mail_pw`. With `server_flags` set to #DC_LP_AUTH_OAUTH2, +/// dc_configure() can be called as usual afterwards. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param addr E-mail address the user has entered. +/// In case the user selects a different e-mail-address during +/// authorization, this is corrected in [dc_configure] +/// @param redirect_uri URL that will get `code` that is used as `mail_pw` then. +/// Not all URLs are allowed here, however, the following should work: +/// `chat.delta:/PATH`, `http://localhost:PORT/PATH`, +/// `https://localhost:PORT/PATH`, `urn:ietf:wg:oauth:2.0:oob` +/// (the latter just displays the code the user can copy+paste then) +/// +/// Returns URL that can be opened in the browser to start OAuth2. +/// If OAuth2 is not possible for the given e-mail-address, NULL is returned. #[no_mangle] pub unsafe extern "C" fn dc_get_oauth2_url( context: *mut dc_context_t, @@ -153,149 +472,538 @@ pub unsafe extern "C" fn dc_get_oauth2_url( redirect: *mut libc::c_char, ) -> *mut libc::c_char { assert!(!context.is_null()); - - let context = &*context; - let addr = dc_tools::to_string(addr); - let redirect = dc_tools::to_string(redirect); + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); + let addr = to_string(addr); + let redirect = to_string(redirect); match oauth2::dc_get_oauth2_url(context, addr, redirect) { Some(res) => res.strdup(), None => std::ptr::null_mut(), } } +/// Find out the version of the Delta Chat core library. +/// +/// Deprecated, use dc_get_config() instead. +/// +/// @memberof [dc_context_t] +/// +/// Returns string with version number as `major.minor.revision`. The +/// return value must be free()'d. #[no_mangle] pub unsafe extern "C" fn dc_get_version_str() -> *mut libc::c_char { context::dc_get_version_str() } +/// Configures a context. +/// +/// For this purpose, the function creates a job that is executed in +/// the IMAP-thread then; this requires to call dc_perform_imap_jobs() +/// regularly. If the context is already configured, this function +/// will try to change the configuration. +/// +/// * Before you call this function, you must set at least `addr` and +/// `mail_pw` using dc_set_config(). +/// +/// * Use `mail_user` to use a different user name than `addr` and +/// `send_pw` to use a different password for the SMTP server. +/// +/// * If _no_ more options are specified, +/// the function **uses autoconfigure/autodiscover** +/// to get the full configuration from well-known URLs. +/// +/// * If _more_ options as `mail_server`, `mail_port`, `send_server`, +/// `send_port`, `send_user` or `server_flags` are specified, +/// **autoconfigure/autodiscover is skipped**. +/// +/// While this function returns immediately, the started +/// configuration-job may take a while. +/// +/// During configuration, #DC_EVENT_CONFIGURE_PROGRESS events are +/// emitted; they indicate a successful configuration as well as +/// errors and may be used to create a progress bar. +/// +/// Additional calls to this function while a config-job is running +/// are ignored. To interrupt a configuration prematurely, use +/// [dc_stop_ongoing_process]; this is not needed if +/// #DC_EVENT_CONFIGURE_PROGRESS reports success. +/// +/// On a successfull configuration, the core makes a copy of the +/// parameters mentioned above: the original parameters as are never +/// modified by the core. +/// +/// UI-implementors should keep this in mind - eg. if the UI wants to +/// prefill a configure-edit-dialog with these parameters, the UI +/// should reset them if the user cancels the dialog after a +/// configure-attempts has failed. Otherwise the parameters may not +/// reflect the current configuration. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// +/// There is no need to call [dc_configure] on every program start, +/// the configuration result is saved in the database and you can use +/// the connection directly: +/// +/// ``` +/// if (!dc_is_configured(context)) { +/// dc_configure(context); +/// // wait for progress events +/// } +/// ``` #[no_mangle] pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); configure::configure(context) } +/// Checks if the context is already configured. +/// +/// Typically, for unconfigured accounts, the user is prompted to +/// enter some settings and [dc_configure] is called in a thread then. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by dc_context_new(). +/// +///Returns `1` if the context is configured and can be used; `0` if +/// the context is not configured and a configuration by +/// [dc_configure] is required. #[no_mangle] pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); configure::dc_is_configured(context) } +/// Executes pending imap-jobs. +/// +/// This function and [dc_perform_imap_fetch] and [dc_perform_imap_idle] +/// must be called from the same thread, typically in a loop. +/// +/// Example: +/// +/// ``` +/// void* imap_thread_func(void* context) +/// { +/// while (true) { +/// dc_perform_imap_jobs(context); +/// dc_perform_imap_fetch(context); +/// dc_perform_imap_idle(context); +/// } +/// } +/// +/// // start imap-thread that runs forever +/// pthread_t imap_thread; +/// pthread_create(&imap_thread, NULL, imap_thread_func, context); +/// +/// ... program runs ... +/// +/// // network becomes available again - +/// // the interrupt causes dc_perform_imap_idle() in the thread above +/// // to return so that jobs are executed and messages are fetched. +/// dc_maybe_network(context); +/// ``` +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_imap_jobs(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_imap_jobs(context) } +/// Fetches new messages, if any. +/// +/// This function and [dc_perform_imap_jobs] and +/// [dc_perform_imap_idle] must be called from the same thread, +/// typically in a loop. +/// +/// See [dc_perform_imap_jobs] for an example. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_imap_fetch(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_imap_fetch(context) } +/// Waits for messages or jobs. +/// +/// This function and [dc_perform_imap_jobs] and +/// [dc_perform_imap_fetch] must be called from the same thread, +/// typically in a loop. +/// +/// You should call this function directly after calling [dc_perform_imap_fetch]. +/// +/// See [dc_perform_imap_jobs] for an example. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_imap_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_imap_idle(context) } +/// Interrupts waiting for imap-jobs. +/// +/// If dc_perform_imap_jobs(), dc_perform_imap_fetch() and +/// dc_perform_imap_idle() are called in a loop, calling this function +/// causes imap-jobs to be executed and messages to be fetched. +/// +/// [dc_interrupt_imap_idle] does _not_ [interrupt +/// dc_perform_imap_jobs] or [dc_perform_imap_fetch]. If the +/// imap-thread is inside one of these functions when +/// [dc_interrupt_imap_idle] is called, however, the next call of the +/// imap-thread to [dc_perform_imap_idle] is interrupted immediately. +/// +/// Internally, this function is called whenever a imap-jobs should be +/// processed (delete message, markseen etc.). +/// +/// When you need to call this function just because to get jobs done +/// after network changes, use [dc_maybe_network] instead. +/// +/// @memberof [dc_context_t] +/// +/// # Parameters +/// +/// * `context` - The context as created by [dc_context_new]. +/// +/// # Panics +/// +/// It is safe to call this function on a closed context. #[no_mangle] pub unsafe extern "C" fn dc_interrupt_imap_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - - job::interrupt_imap_idle(context) + let wrapper: &ContextWrapper = &*context; + if wrapper.inner.is_some() { + let context = wrapper.inner.as_ref().expect("context not open"); + job::interrupt_imap_idle(context); + } } +/// Fetches new messages from the MVBOX, if any. +/// +/// The MVBOX is a folder on the account where chat messages are moved +/// to. The moving is done to not disturb shared accounts that are +/// used by both, Delta Chat and a classical MUA. +/// +/// This function and [dc_perform_mvbox_idle] must be called from the +/// same thread, typically in a loop. +/// +/// Example: +/// +/// ``` +/// void* mvbox_thread_func(void* context) +/// { +/// while (true) { +/// dc_perform_mvbox_fetch(context); +/// dc_perform_mvbox_idle(context); +/// } +/// } +/// +/// // start mvbox-thread that runs forever +/// pthread_t mvbox_thread; +/// pthread_create(&mvbox_thread, NULL, mvbox_thread_func, context); +/// +/// ... program runs ... +/// +/// // network becomes available again - +/// // the interrupt causes dc_perform_mvbox_idle() in the thread above +/// // to return so that and messages are fetched. +/// dc_maybe_network(context); +/// ``` +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by dc_context_new(). #[no_mangle] pub unsafe extern "C" fn dc_perform_mvbox_fetch(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_mvbox_fetch(context) } +/// Waits for messages or jobs in the MVBOX-thread. +/// +/// This function and [dc_perform_mvbox_fetch]. must be called from +/// the same thread, typically in a loop. +/// +/// You should call this function directly after calling +/// [dc_perform_mvbox_fetch]. +/// +/// See [dc_perform_mvbox_fetch] for an example. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_mvbox_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_mvbox_idle(context) } +/// Interrupts waiting for MVBOX-fetch. +/// +/// [dc_interrupt_mvbox_idle] does _not_ interrupt +/// [dc_perform_mvbox_fetch]. If the MVBOX-thread is inside this +/// function when [dc_interrupt_mvbox_idle] is called, however, the +/// next call of the MVBOX-thread to [dc_perform_mvbox_idle] is +/// interrupted immediately. +/// +/// Internally, this function is called whenever a imap-jobs should be +/// processed. +/// +/// When you need to call this function just because to get jobs done +/// after network changes, use [dc_maybe_network] instead. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_interrupt_mvbox_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::interrupt_mvbox_idle(context) } +/// Fetches new messages from the Sent folder, if any. +/// +/// This function and [dc_perform_sentbox_idle] must be called from +/// the same thread, typically in a loop. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_sentbox_fetch(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_sentbox_fetch(context) } +/// Waits for messages or jobs in the SENTBOX-thread. +/// +/// This function and [dc_perform_sentbox_fetch] must be called from +/// the same thread, typically in a loop. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_sentbox_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_sentbox_idle(context) } +/// Interrupts waiting for messages or jobs in the SENTBOX-thread. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_interrupt_sentbox_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::interrupt_sentbox_idle(context) } +/// Executes pending smtp-jobs. +/// +/// This function and dc_perform_smtp_idle() must be called from the +/// same thread, typically in a loop. +/// +/// Example: +/// +/// ``` +/// void* smtp_thread_func(void* context) +/// { +/// while (true) { +/// dc_perform_smtp_jobs(context); +/// dc_perform_smtp_idle(context); +/// } +/// } +/// +/// // start smtp-thread that runs forever +/// pthread_t smtp_thread; +/// pthread_create(&smtp_thread, NULL, smtp_thread_func, context); +/// +/// ... program runs ... +/// +/// // network becomes available again - +/// // the interrupt causes dc_perform_smtp_idle() in the thread above +/// // to return so that jobs are executed +/// dc_maybe_network(context); +/// ``` +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_smtp_jobs(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_smtp_jobs(context) } +/// Waits for smtp-jobs. +/// +/// This function and [dc_perform_smtp_jobs] must be called from the +/// same thread, typically in a loop. +/// +/// See [dc_interrupt_smtp_idle] for an example. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_perform_smtp_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::perform_smtp_idle(context) } +/// Interrupts waiting for smtp-jobs. +/// +/// If [dc_perform_smtp_jobs] and [dc_perform_smtp_idle] are called in +/// a loop, calling this function causes jobs to be executed. +/// +/// [dc_interrupt_smtp_idle] does _not_ interrupt +/// [dc_perform_smtp_jobs]. If the smtp-thread is inside this +/// function when [dc_interrupt_smtp_idle] is called, however, the +/// next call of the smtp-thread to [dc_perform_smtp_idle] is +/// interrupted immediately. +/// +/// Internally, this function is called whenever a message is to be +/// sent. +/// +/// When you need to call this function just because to get jobs done +/// after network changes, use [dc_maybe_network] instead. +/// +/// @memberof [dc_context_t] +/// +/// # Arguments +/// +/// * `context` - The context as created by [dc_context_new]. +/// +/// # Panics +/// +/// It is safe to call this function on a closed context. #[no_mangle] pub unsafe extern "C" fn dc_interrupt_smtp_idle(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - - job::interrupt_smtp_idle(context) + let wrapper: &ContextWrapper = &*context; + if wrapper.inner.is_some() { + let context = wrapper.inner.as_ref().expect("context not open"); + job::interrupt_smtp_idle(context) + } } +/// Signals possible network availability. +/// +/// This function can be called whenever there is a hint. that the +/// network is available again. The library will try to send pending +/// messages out. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); job::maybe_network(context) } +/// Gets a list of chats. +/// +/// The list can be filtered by query parameters. +/// +/// The list is already sorted and starts with the most recent chat in +/// use. The sorting takes care of invalid sending dates, drafts and +/// chats without messages. Clients should not try to re-sort the +/// list as this would be an expensive action and would result in +/// inconsistencies between clients. +/// +/// To get information about each entry, use +/// eg. [dc_chatlist_get_summary]. +/// +/// By default, the function adds some special entries to the list. +/// These special entries can be identified by the ID returned by +/// [dc_chatlist_get_chat_id]: +/// +/// * DC_CHAT_ID_DEADDROP (1) - this special chat is present if there +/// are messages from addresses that have no relationship to the +/// configured account. The last of these messages is represented +/// by DC_CHAT_ID_DEADDROP and you can retrieve details about it +/// with [dc_chatlist_get_msg_id]. Typically, the UI asks the user +/// "Do you want to chat with NAME?" and offers the options "Yes" +/// (call [dc_create_chat_by_msg_id]), "Never" (call +/// [dc_block_contact]) or "Not now". The UI can also offer a +/// "Close" button that calls [dc_marknoticed_contact] then. +/// +/// * DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if +/// the user has archived _any_ chat using [dc_archive_chat]. The UI +/// should show a link as "Show archived chats", if the user clicks +/// this item, the UI should show a list of all archived chats that +/// can be created by this function hen using the +/// DC_GCL_ARCHIVED_ONLY flag. +/// +/// * DC_CHAT_ID_ALLDONE_HINT (7) - this special chat is present if +/// DC_GCL_ADD_ALLDONE_HINT is added to listflags and if there are +/// only archived chats. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned by [dc_context_new] +/// @param listflags A combination of flags: +/// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned. +/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and +/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived +/// chats +/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added +/// to the list (may be used eg. for selecting chats on forwarding, the flag is +/// not needed when DC_GCL_ARCHIVED_ONLY is already set) +/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT +/// is added as needed. +/// @param query_str An optional query for filtering the list. Only +/// chats matching this query are returned. Give NULL for no +/// filtering. +/// @param query_id An optional contact ID for filtering the list. +/// Only chats including this contact ID are returned. Give 0 for +/// no filtering. +/// +/// Returns a chatlist as a [dc_chatlist_t] object. On errors, NULL is +/// returned. Must be freed using [dc_chatlist_unref] when no longer +/// used. +/// +/// See also: [dc_get_chat_msgs] to get the messages of a single chat. #[no_mangle] pub unsafe extern "C" fn dc_get_chatlist<'a>( context: *mut dc_context_t, @@ -304,8 +1012,8 @@ pub unsafe extern "C" fn dc_get_chatlist<'a>( query_id: u32, ) -> *mut dc_chatlist_t<'a> { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let qs = if query_str.is_null() { None } else { @@ -318,38 +1026,131 @@ pub unsafe extern "C" fn dc_get_chatlist<'a>( } } +/// Create a normal chat or a group chat by a messages ID that comes +/// typically from the deaddrop, DC_CHAT_ID_DEADDROP (1). +/// +/// If the given message ID already belongs to a normal chat or to a +/// group chat, the chat ID of this chat is returned and no new chat +/// is created. If a new chat is created, the given message ID is +/// moved to this chat, however, there may be more messages moved to +/// the chat from the deaddrop. To get the chat messages, use +/// [dc_get_chat_msgs]. +/// +/// If the user is asked before creation, they should be asked whether +/// they wants to chat with the _contact_ belonging to the message; the +/// group names may be really weird when taken from the subject of +/// implicit groups and this may look confusing. +/// +/// Moreover, this function also scales up the origin of the contact belonging +/// to the message and, depending on the contacts origin, messages from the +/// same group may be shown or not - so, all in all, it is fine to show the +/// contact name only. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param msg_id The message ID to create the chat for. +/// +/// Returns the created or reused chat ID on success. `0` on errors. #[no_mangle] pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, msg_id: u32) -> u32 { assert!(!context.is_null()); - let context = &*context; - - chat::create_by_msg_id(context, msg_id).unwrap_or_log_default(context, "Failed to create chat") + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); + chat::create_by_msg_id(context, msg_id).unwrap_or_log_default(context, "Failed to create chag") } +/// Create a normal chat with a single user. +/// +/// To create group chats, see [dc_create_group_chat]. +/// +/// If a chat already exists, this ID is returned, otherwise a new +/// chat is created; this new chat may already contain messages, +/// eg. from the deaddrop, to get the chat messages, use +/// [dc_get_chat_msgs]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param contact_id The contact ID to create the chat for. If there is already +/// a chat with this contact, the already existing ID is returned. +/// +/// Returns the created or reused chat ID on success. `0` on errors. #[no_mangle] pub unsafe extern "C" fn dc_create_chat_by_contact_id( context: *mut dc_context_t, contact_id: u32, ) -> u32 { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::create_by_contact_id(context, contact_id) .unwrap_or_log_default(context, "Failed to create chat") } +/// Check, if there is a normal chat with a given contact. +/// +/// To get the chat messages, use [dc_get_chat_msgs]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param contact_id The contact ID to check. +/// +/// If there is a normal chat with the given contact_id, this chat_id +/// is returned. If there is no normal chat with the contact_id, the +/// function returns `0`. #[no_mangle] pub unsafe extern "C" fn dc_get_chat_id_by_contact_id( context: *mut dc_context_t, contact_id: u32, ) -> u32 { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::get_by_contact_id(context, contact_id) .unwrap_or_log_default(context, "Failed to get chat") } +/// Prepares a message for sending. +/// +/// Call this function if the file to be sent is still in creation. +/// Once you're done with creating the file, call [dc_send_msg] as +/// usual and the message will really be sent. +/// +/// This is useful as the user can already send the next messages +/// while e.g. the recoding of a video is not yet finished. Or the +/// user can even forward the message with the file being still in +/// creation to other groups. +/// +/// Files being sent with the increation-method must be placed in the +/// blob directory, see [dc_get_blobdir]. If the increation-method is +/// not used - which is probably the normal case - dc_send_msg() +/// copies the file to the blob directory if it is not yet there. To +/// distinguish the two cases, msg->state must be set properly. The +/// easiest way to ensure this is to re-use the same object for both +/// calls. +/// +/// # Example +/// +/// ```c +/// dc_msg_t* msg = dc_msg_new(context, DC_MSG_VIDEO); +/// dc_msg_set_file(msg, "/file/to/send.mp4", NULL); +/// dc_prepare_msg(context, chat_id, msg); +/// // ... after /file/to/send.mp4 is ready: +/// dc_send_msg(context, chat_id, msg); +/// ``` +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id Chat ID to send the message to. +/// @param msg Message object to send to the chat defined by the chat ID. +/// On succcess, `msg_id` and state of the object are set up, +/// The function does not take ownership of the object, +/// so you have to free it using [dc_msg_unref] as usual. +/// +/// Retuns the ID of the message that is being prepared. #[no_mangle] pub unsafe extern "C" fn dc_prepare_msg( context: *mut dc_context_t, @@ -358,13 +1159,40 @@ pub unsafe extern "C" fn dc_prepare_msg( ) -> u32 { assert!(!context.is_null()); assert!(!msg.is_null()); - let context = &mut *context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let msg = &mut *msg; chat::prepare_msg(context, chat_id, msg) .unwrap_or_log_default(context, "Failed to prepare message") } +/// Sends a message defined by a dc_msg_t object to a chat. +/// +/// Sends the event #DC_EVENT_MSGS_CHANGED on succcess. However, this +/// does not imply, the message really reached the recipient - sending +/// may be delayed eg. due to network problems. However, from your +/// view, you're done with the message. Sooner or later it will find +/// its way. +/// +/// # Example +/// +/// ```c +/// dc_msg_t* msg = dc_msg_new(context, DC_MSG_IMAGE); +/// dc_msg_set_file(msg, "/file/to/send.jpg", NULL); +/// dc_send_msg(context, chat_id, msg); +/// ``` +/// +/// @memberof [dc_context_t] +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id Chat ID to send the message to. +/// If [dc_prepare_msg] was called before, this parameter can be 0. +/// @param msg Message object to send to the chat defined by the chat ID. +/// On succcess, msg_id of the object is set up, +/// The function does not take ownership of the object, +/// so you have to free it using [dc_msg_unref] as usual. +/// +/// Returns the ID of the message that is about to be sent. `0` in +/// case of errors. #[no_mangle] pub unsafe extern "C" fn dc_send_msg( context: *mut dc_context_t, @@ -373,12 +1201,32 @@ pub unsafe extern "C" fn dc_send_msg( ) -> u32 { assert!(!context.is_null()); assert!(!msg.is_null()); - let context = &mut *context; + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let msg = &mut *msg; - chat::send_msg(context, chat_id, msg).unwrap_or_log_default(context, "Failed to send message") } +/// Sends a simple text message a given chat. +/// +/// Sends the event #DC_EVENT_MSGS_CHANGED on succcess. However, this +/// does not imply, the message really reached the recipient - sending +/// may be delayed eg. due to network problems. However, from your +/// view, you're done with the message. Sooner or later it will find +/// its way. +/// +/// See also [dc_send_msg]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id Chat ID to send the text message to. +/// @param text_to_send Text to send to the chat defined by the chat ID. +/// Passing an empty text here causes an empty text to be sent, +/// it's up to the caller to handle this if undesired. +/// Passing NULL as the text causes the function to return 0. +/// +/// Returns the ID of the message that is about being sent. #[no_mangle] pub unsafe extern "C" fn dc_send_text_msg( context: *mut dc_context_t, @@ -387,13 +1235,38 @@ pub unsafe extern "C" fn dc_send_text_msg( ) -> u32 { assert!(!context.is_null()); assert!(!text_to_send.is_null()); - let context = &*context; + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let text_to_send = dc_tools::to_string_lossy(text_to_send); - chat::send_text_msg(context, chat_id, text_to_send) .unwrap_or_log_default(context, "Failed to send text message") } +/// Saves a draft for a chat in the database. +// +/// The UI should call this function if the user has prepared a +/// message and exits the compose window without clicking the "send" +/// button before. When the user later opens the same chat again, the +/// UI can load the draft using [dc_get_draft] allowing the user to +/// continue editing and sending. +/// +/// Drafts are considered when sorting messages +/// and are also returned eg. by [dc_chatlist_get_summary]. +/// +/// Each chat can have its own draft but only one draft per chat is +/// possible. +/// +/// If the draft is modified, an #DC_EVENT_MSGS_CHANGED will be sent. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param chat_id The chat ID to save the draft for. +/// @param msg The message to save as a draft. +/// Existing draft will be overwritten. +/// NULL deletes the existing draft, if any, without sending it. +/// Currently, also non-text-messages +/// will delete the existing drafts. #[no_mangle] pub unsafe extern "C" fn dc_set_draft( context: *mut dc_context_t, @@ -401,23 +1274,60 @@ pub unsafe extern "C" fn dc_set_draft( msg: *mut dc_msg_t, ) { assert!(!context.is_null()); - let context = &*context; + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let msg = if msg.is_null() { None } else { Some(&mut *msg) }; - chat::set_draft(context, chat_id, msg) } +/// Gets draft for a chat, if any. +/// +/// See [dc_set_draft] for more details about drafts. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param chat_id The chat ID to get the draft for. +/// +/// Returns a message object. Can be passed directly to +/// [dc_send_msg]. Must be freed using [dc_msg_unref] after usage. +/// If there is no draft, NULL is returned. #[no_mangle] pub unsafe extern "C" fn dc_get_draft<'a>( context: *mut dc_context_t, chat_id: u32, ) -> *mut dc_msg_t<'a> { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::get_draft(context, chat_id).into_raw() } +/// Gets all message IDs belonging to a chat. +/// +/// The list is already sorted and starts with the oldest message. +/// Clients should not try to re-sort the list as this would be an +/// expensive action and would result in inconsistencies between +/// clients. +/// +/// Optionally, some special markers added to the ID-array may help to +/// implement virtual lists. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The chat ID of which the messages IDs should be queried. +/// @param flags If set to DC_GCM_ADDDAYMARKER, the marker +/// DC_MSG_ID_DAYMARKER will be added before each day (regarding +/// the local timezone). Set this to 0 if you do not want this +/// behaviour. +/// @param marker1before An optional message ID. If set, the id +/// DC_MSG_ID_MARKER1 will be added just before the given ID in +/// the returned array. Set this to 0 if you do not want this +/// behaviour. +/// +/// Returns an array of message IDs, must be [dc_array_unref]'d when +/// no longer used. #[no_mangle] pub unsafe extern "C" fn dc_get_chat_msgs( context: *mut dc_context_t, @@ -426,55 +1336,106 @@ pub unsafe extern "C" fn dc_get_chat_msgs( marker1before: u32, ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let arr = dc_array_t::from(chat::get_chat_msgs(context, chat_id, flags, marker1before)); Box::into_raw(Box::new(arr)) } +/// Gets the total number of messages in a chat. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The ID of the chat to count the messages for. +/// +/// Returns the number of total messages in the given chat. `0` for +/// errors or empty chats. #[no_mangle] pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::get_msg_cnt(context, chat_id) as libc::c_int } +/// Gets the number of _fresh_ messages in a chat. +/// +/// Typically used to implement a badge with a number in the chatlist. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The ID of the chat to count the messages for. +/// +/// Returns the number of fresh messages in the given chat. `0` for +/// errors or if there are no fresh messages. #[no_mangle] pub unsafe extern "C" fn dc_get_fresh_msg_cnt( context: *mut dc_context_t, chat_id: u32, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::get_fresh_msg_cnt(context, chat_id) as libc::c_int } +/// Returns the message IDs of all _fresh_ messages of any chat. +/// +/// Typically used for implementing notification summaries. The list +/// is already sorted and starts with the most recent fresh message. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// +/// Returns an array of message IDs, must be [dc_array_unref]'d when +/// no longer used. On errors, the list is empty. NULL is never +/// returned. #[no_mangle] pub unsafe extern "C" fn dc_get_fresh_msgs( context: *mut dc_context_t, ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let arr = dc_array_t::from(context::dc_get_fresh_msgs(context)); Box::into_raw(Box::new(arr)) } +/// Mark all messages in a chat as _noticed_. +/// +/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen +/// but are still waiting for being marked as "seen" using [dc_markseen_msgs] +/// (IMAP/MDNs is not done for noticed messages). +/// +/// Calling this function usually results in the event +/// #DC_EVENT_MSGS_CHANGED. See also [dc_marknoticed_all_chats], +/// [dc_marknoticed_contact] and [dc_markseen_msgs]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The chat ID of which all messages should be marked as being noticed. #[no_mangle] pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id: u32) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::marknoticed_chat(context, chat_id).log_err(context, "Failed marknoticed chat"); } +/// Same as dc_marknoticed_chat() but for _all_ chats. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. #[no_mangle] pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::marknoticed_all_chats(context).log_err(context, "Failed marknoticed all chats"); } @@ -486,6 +1447,26 @@ where FromPrimitive::from_i64(s.into()) } +/// Returns all message IDs of the given types in a chat. +/// +/// Typically used to show a gallery. +/// The result must be [dc_array_unref]'d +/// +/// The list is already sorted and starts with the oldest message. +/// Clients should not try to re-sort the list as this would be an +/// expensive action and would result in inconsistencies between +/// clients. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The chat ID to get all messages with media from. +/// @param msg_type Specify a message type to query here, one of the DC_MSG_* constats. +/// @param msg_type2 Alternative message type to search for. 0 to skip. +/// @param msg_type3 Alternative message type to search for. 0 to skip. +/// +/// Returns an array with messages from the given chat ID that have +/// the wanted message types. #[no_mangle] pub unsafe extern "C" fn dc_get_chat_media( context: *mut dc_context_t, @@ -495,7 +1476,8 @@ pub unsafe extern "C" fn dc_get_chat_media( or_msg_type3: libc::c_int, ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type)); let or_msg_type2 = @@ -513,6 +1495,27 @@ pub unsafe extern "C" fn dc_get_chat_media( Box::into_raw(Box::new(arr)) } +/// Search next/previous message based on a given message and a list of types. +/// +/// The Typically used to implement the "next" and "previous" buttons +/// in a gallery or in a media player. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param curr_msg_id This is the current message +/// from which the next or previous message should be searched. +/// @param dir 1=get the next message, -1=get the previous one. +/// @param msg_type Message type to search for. +/// If 0, the message type from curr_msg_id is used. +/// @param msg_type2 Alternative message type to search for. 0 to skip. +/// @param msg_type3 Alternative message type to search for. 0 to skip. +/// +/// Returns the message ID that should be played next. The returned +/// message is in the same chat as the given one and has one of the +/// given types. Typically, this result is passed again to +/// [dc_get_next_media] later on the next swipe. If there is not +/// next/previous message, the function returns 0. #[no_mangle] pub unsafe extern "C" fn dc_get_next_media( context: *mut dc_context_t, @@ -523,7 +1526,8 @@ pub unsafe extern "C" fn dc_get_next_media( or_msg_type3: libc::c_int, ) -> u32 { assert!(!context.is_null()); - let context = &*context; + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type)); let or_msg_type2 = @@ -534,6 +1538,31 @@ pub unsafe extern "C" fn dc_get_next_media( chat::get_next_media(context, msg_id, dir, msg_type, or_msg_type2, or_msg_type3) } +/// Archives or unarchives a chat. +/// +/// Archived chats are not included in the default chatlist returned +/// by dc_get_chatlist(). Instead, if there are _any_ archived chats, +/// the pseudo-chat with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be +/// added the the end of the chatlist. +/// +/// * To get a list of archived chats, use dc_get_chatlist() with the +/// flag DC_GCL_ARCHIVED_ONLY. +/// +/// * To find out the archived state of a given chat, use +/// [dc_chat_get_archived] +/// +/// * Messages in archived chats are marked as being noticed, so they +/// do not count as "fresh" +/// +/// * Calling this function usually results in the event +/// #DC_EVENT_MSGS_CHANGED +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The ID of the chat to archive or unarchive. +/// @param archive 1=archive chat, 0=unarchive chat, all other values +/// are reserved for future use #[no_mangle] pub unsafe extern "C" fn dc_archive_chat( context: *mut dc_context_t, @@ -541,8 +1570,8 @@ pub unsafe extern "C" fn dc_archive_chat( archive: libc::c_int, ) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let archive = if archive == 0 { false } else if archive == 1 { @@ -550,30 +1579,93 @@ pub unsafe extern "C" fn dc_archive_chat( } else { return; }; - chat::archive(context, chat_id, archive).log_err(context, "Failed archive chat"); } +/// Deletes a chat. +/// +/// Messages are deleted from the device and the chat database entry +/// is deleted. After that, the event #DC_EVENT_MSGS_CHANGED is +/// posted. +/// +/// Things that are _not_ done implicitly: +/// +/// * Messages are **not deleted from the server**. +/// * The chat or the contact is **not blocked**, so new messages from +/// the user/the group may appear and the user may create the chat +/// again. +/// * **Groups are not left** - this would be unexpected as (1) +/// deleting a normal chat also does not prevent new mails from +/// arriving, (2) leaving a group requires sending a message to all +/// group members - especially for groups not used for a longer +/// time, this is really unexpected when deletion results in +/// contacting all members again, (3) only leaving groups is also a +/// valid usecase. +/// +/// To leave a chat explicitly, use dc_remove_contact_from_chat() with +/// chat_id=DC_CONTACT_ID_SELF) +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The ID of the chat to delete. #[no_mangle] pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::delete(context, chat_id).log_err(context, "Failed chat delete"); } +/// Gets contact IDs belonging to a chat. +/// +/// * for normal chats, the function always returns exactly one +/// contact, DC_CONTACT_ID_SELF is returned only for SELF-chats. +/// +/// * for group chats all members are returned, DC_CONTACT_ID_SELF is +/// returned explicitly as it may happen that oneself gets removed +/// from a still existing group +/// +/// * for the deaddrop, the list is empty +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id Chat ID to get the belonging contact IDs for. +/// +/// Returns an array of contact IDs belonging to the chat; must be +/// freed using [dc_array_unref] when done. #[no_mangle] pub unsafe extern "C" fn dc_get_chat_contacts( context: *mut dc_context_t, chat_id: u32, ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let arr = dc_array_t::from(chat::get_chat_contacts(context, chat_id)); Box::into_raw(Box::new(arr)) } +/// Searches messages containing the given query string. +/// +/// Searching can be done globally (chat_id=0) or in a specified chat +/// only (chat_id set). +/// +/// Global chat results are typically displayed using +/// [dc_msg_get_summary], chat search results may just hilite the +/// corresponding messages and present a prev/next button. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id ID of the chat to search messages in. +/// Set this to 0 for a global search. +/// @param query The query to search for. +/// +/// Returns an array of message IDs. Must be freed using +/// [dc_array_unref] when no longer needed. If nothing can be found, +/// the function returns NULL. #[no_mangle] pub unsafe extern "C" fn dc_search_msgs( context: *mut dc_context_t, @@ -582,26 +1674,61 @@ pub unsafe extern "C" fn dc_search_msgs( ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); assert!(!query.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let arr = dc_array_t::from(context::dc_search_msgs(context, chat_id, query)); Box::into_raw(Box::new(arr)) } +/// Gets chat object by a chat ID. +/// +/// @memberof [dc_context_t] +/// @param context The context object as returned from [dc_context_new]. +/// @param chat_id The ID of the chat to get the chat object for. +/// +/// Returns a chat object of the type dc_chat_t, must be freed using +/// [dc_chat_unref] when done. On errors, NULL is returned. #[no_mangle] pub unsafe extern "C" fn dc_get_chat<'a>( context: *mut dc_context_t, chat_id: u32, ) -> *mut dc_chat_t<'a> { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); match chat::Chat::load_from_db(context, chat_id) { Ok(chat) => Box::into_raw(Box::new(chat)), Err(_) => std::ptr::null_mut(), } } +/// Creates a new group chat. +/// +/// After creation, the draft of the chat is set to a default text, +/// the group has one member with the ID DC_CONTACT_ID_SELF and is in +/// _unpromoted_ state. This means, you can add or remove members, +/// change the name, the group image and so on without messages being +/// sent to all group members. +/// +/// This changes as soon as the first message is sent to the group +/// members and the group becomes _promoted_. After that, all changes +/// are synced with all group members by sending status message. +/// +/// To check, if a chat is still unpromoted, you +/// [dc_chat_is_unpromoted]. This may be useful if you want to show +/// some help for just created groups. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param verified If set to 1 the function creates a secure verified group. +/// Only secure-verified members are allowed in these groups +/// and end-to-end-encryption is always enabled. +/// @param chat_name The name of the group chat to create. +/// The name may be changed later using [dc_set_chat_name]. +/// To find out the name of a group later, see [dc_chat_get_name] +/// +/// Returns the chat ID of the new group chat, `0` on errors. #[no_mangle] pub unsafe extern "C" fn dc_create_group_chat( context: *mut dc_context_t, @@ -610,18 +1737,28 @@ pub unsafe extern "C" fn dc_create_group_chat( ) -> u32 { assert!(!context.is_null()); assert!(!name.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let verified = if let Some(s) = contact::VerifiedStatus::from_i32(verified) { s } else { return 0; }; - chat::create_group_chat(context, verified, as_str(name)) .unwrap_or_log_default(context, "Failed to create group chat") } +/// Checks if a given contact ID is a member of a group chat. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param chat_id The chat ID to check. +/// @param contact_id The contact ID to check. To check if yourself is member +/// of the chat, pass DC_CONTACT_ID_SELF (1) here. +/// +/// Returns `1` if contact ID is member of chat ID, `0` if contact is +/// not in chat. #[no_mangle] pub unsafe extern "C" fn dc_is_contact_in_chat( context: *mut dc_context_t, @@ -629,11 +1766,30 @@ pub unsafe extern "C" fn dc_is_contact_in_chat( contact_id: u32, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::is_contact_in_chat(context, chat_id, contact_id) } +/// Adds a member to a group. +/// +/// If the group is already _promoted_ (any message was sent to the +/// group), all group members are informed by a special status message +/// that is sent automatically by this function. +/// +/// If the group is a verified group, only verified contacts can be +/// added to the group. +/// +/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a +/// status message was sent. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by dc_context_new(). +/// @param chat_id The chat ID to add the contact to. Must be a group chat. +/// @param contact_id The contact ID to add to the chat. +/// +/// Returns `1` if the member added to group, `0` on error. #[no_mangle] pub unsafe extern "C" fn dc_add_contact_to_chat( context: *mut dc_context_t, @@ -641,11 +1797,27 @@ pub unsafe extern "C" fn dc_add_contact_to_chat( contact_id: u32, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::add_contact_to_chat(context, chat_id, contact_id) } +/// Removes a member from a group. +/// +/// If the group is already _promoted_ (any message was sent to the +/// group), all group members are informed by a special status message +/// that is sent automatically by this function. +/// +/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a +/// status message was sent. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param chat_id The chat ID to remove the contact from. Must be a group chat. +/// @param contact_id The contact ID to remove from the chat. +/// +/// Returns `1` if the member is removed from group, `0` on error. #[no_mangle] pub unsafe extern "C" fn dc_remove_contact_from_chat( context: *mut dc_context_t, @@ -653,13 +1825,29 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat( contact_id: u32, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::remove_contact_from_chat(context, chat_id, contact_id) .map(|_| 1) .unwrap_or_log_default(context, "Failed to remove contact") } +/// Sets group name. +/// +/// If the group is already _promoted_ (any message was sent to the +/// group), all group members are informed by a special status message +/// that is sent automatically by this function. +/// +/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a +/// status message was sent. +/// +/// @memberof [dc_context_t] +/// +/// @param chat_id The chat ID to set the name for. Must be a group chat. +/// @param new_name New name of the group. +/// @param context The context as created by [dc_context_new]. +/// +/// Returns `1` on success, `0` on error. #[no_mangle] pub unsafe extern "C" fn dc_set_chat_name( context: *mut dc_context_t, @@ -669,13 +1857,34 @@ pub unsafe extern "C" fn dc_set_chat_name( assert!(!context.is_null()); assert!(!name.is_null()); assert!(chat_id > constants::DC_CHAT_ID_LAST_SPECIAL as u32); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::set_chat_name(context, chat_id, as_str(name)) .map(|_| 1) .unwrap_or_log_default(context, "Failed to set chat name") } +/// Sets group profile image. +/// +/// If the group is already _promoted_ (any message was sent to the +/// group), all group members are informed by a special status message +/// that is sent automatically by this function. +/// +/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a +/// status message was sent. +/// +/// To find out the profile image of a chat, use +/// [dc_chat_get_profile_image]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param chat_id The chat ID to set the image for. +/// @param new_image Full path of the image to use as the group image. +/// If you pass NULL here, the group image is deleted (for +/// promoted groups, all members are informed about this change +/// anyway). +/// Returns `1` on success, `0` on error. #[no_mangle] pub unsafe extern "C" fn dc_set_chat_profile_image( context: *mut dc_context_t, @@ -684,35 +1893,75 @@ pub unsafe extern "C" fn dc_set_chat_profile_image( ) -> libc::c_int { assert!(!context.is_null()); assert!(chat_id > constants::DC_CHAT_ID_LAST_SPECIAL as u32); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::set_chat_profile_image(context, chat_id, as_str(image)) .map(|_| 1) .unwrap_or_log_default(context, "Failed to set profile image") } +/// Gets an informational text for a single message. +/// +/// The text is multiline and may contain eg. the raw text of the +/// message. +/// +/// The max. text returned is typically longer (about 100000 +/// characters) than the max. text returned by [dc_msg_get_text] +/// (about 30000 characters). +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param msg_id The message id for which information should be generated +/// +/// Returns a text string, must be free()'d after usage. #[no_mangle] pub unsafe extern "C" fn dc_get_msg_info( context: *mut dc_context_t, msg_id: u32, ) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); message::dc_get_msg_info(context, msg_id) } +/// Get the raw mime-headers of the given message. +/// +/// Raw headers are saved for incoming messages only if +/// `dc_set_config(context, "save_mime_headers", "1")` was called +/// before. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param msg_id The message id, must be the id of an incoming message. +/// +/// Returns raw headers as a multi-line string, must be free()'d after +/// usage. Returns NULL if there are no headers saved for the given +/// message, eg. because of save_mime_headers is not set or the +/// message is not incoming. #[no_mangle] pub unsafe extern "C" fn dc_get_mime_headers( context: *mut dc_context_t, msg_id: u32, ) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); message::dc_get_mime_headers(context, msg_id) } +/// Delete messages. +/// +/// The messages are deleted on the current device and on the IMAP +/// server. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new] +/// @param msg_ids an array of uint32_t containing all message IDs that should be deleted +/// @param msg_cnt The number of messages IDs in the msg_ids array #[no_mangle] pub unsafe extern "C" fn dc_delete_msgs( context: *mut dc_context_t, @@ -722,11 +1971,20 @@ pub unsafe extern "C" fn dc_delete_msgs( assert!(!context.is_null()); assert!(!msg_ids.is_null()); assert!(msg_cnt > 0); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); message::dc_delete_msgs(context, msg_ids, msg_cnt) } +/// Forward messages to another chat. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new] +/// @param msg_ids An array of uint32_t containing all message IDs +/// that should be forwarded +/// @param msg_cnt The number of messages IDs in the msg_ids array +/// @param chat_id The destination chat ID. #[no_mangle] pub unsafe extern "C" fn dc_forward_msgs( context: *mut dc_context_t, @@ -738,19 +1996,45 @@ pub unsafe extern "C" fn dc_forward_msgs( assert!(!msg_ids.is_null()); assert!(msg_cnt > 0); assert!(chat_id > constants::DC_CHAT_ID_LAST_SPECIAL as u32); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); chat::forward_msgs(context, msg_ids, msg_cnt, chat_id) } +/// Mark all messages sent by the given contact as _noticed_. +/// +/// See also [dc_marknoticed_chat] and [dc_markseen_msgs] +/// +/// Calling this function usually results in the event +/// #DC_EVENT_MSGS_CHANGED. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new] +/// @param contact_id The contact ID of which all messages should be +/// marked as noticed. #[no_mangle] pub unsafe extern "C" fn dc_marknoticed_contact(context: *mut dc_context_t, contact_id: u32) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); Contact::mark_noticed(context, contact_id) } +/// Marks a message as _seen_. +/// +/// Marks a message as seen and updates the IMAP state and sends +/// MDNs. If the message is not in a real chat (eg. a contact +/// request), the message is only marked as NOTICED and no IMAP/MDNs +/// is done. See also [dc_marknoticed_chat] and +/// [dc_marknoticed_contact] +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param msg_ids An array of uint32_t containing all the messages +/// IDs that should be marked as seen. +/// @param msg_cnt The number of message IDs in msg_ids. #[no_mangle] pub unsafe extern "C" fn dc_markseen_msgs( context: *mut dc_context_t, @@ -760,11 +2044,23 @@ pub unsafe extern "C" fn dc_markseen_msgs( assert!(!context.is_null()); assert!(!msg_ids.is_null()); assert!(msg_cnt > 0); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); message::dc_markseen_msgs(context, msg_ids, msg_cnt as usize); } +/// Star/unstar messages by setting the last parameter to 0 (unstar) or 1 (star). +/// +/// Starred messages are collected in a virtual chat that can be shown +/// using [dc_get_chat_msgs] using the chat_id DC_CHAT_ID_STARRED. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new] +/// @param msg_ids An array of uint32_t message IDs defining the +/// messages to star or unstar +/// @param msg_cnt The number of IDs in msg_ids +/// @param star 0=unstar the messages in msg_ids, 1=star them #[no_mangle] pub unsafe extern "C" fn dc_star_msgs( context: *mut dc_context_t, @@ -775,29 +2071,69 @@ pub unsafe extern "C" fn dc_star_msgs( assert!(!context.is_null()); assert!(!msg_ids.is_null()); assert!(msg_cnt > 0); - - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); message::dc_star_msgs(context, msg_ids, msg_cnt, star); } +/// Get a single message object of the type dc_msg_t. +/// +/// For a list of messages in a chat, see [dc_get_chat_msgs] +/// For a list or chats, see [dc_get_chatlist] +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param msg_id The message ID for which the message object should be created. +/// +/// Returns a dc_msg_t message object. On errors, NULL is returned. +/// When done, the object must be freed using [dc_msg_unref]. #[no_mangle] pub unsafe extern "C" fn dc_get_msg<'a>( context: *mut dc_context_t, msg_id: u32, ) -> *mut dc_msg_t<'a> { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); message::dc_get_msg(context, msg_id).into_raw() } +/// Rough check if a string may be a valid e-mail address. +/// +/// The function checks if the string contains a minimal amount of +/// characters before and after the `@` and `.` characters. +/// +/// To check if a given address is a contact in the contact database +/// use [dc_lookup_contact_id_by_addr]. +/// +/// @memberof [dc_context_t] +/// +/// @param addr The e-mail-address to check. +/// +/// Returns `1` if the address may be a valid e-mail address, `0` if +/// address won't be a valid e-mail address. #[no_mangle] pub unsafe extern "C" fn dc_may_be_valid_addr(addr: *mut libc::c_char) -> libc::c_int { assert!(!addr.is_null()); contact::may_be_valid_addr(as_str(addr)) as libc::c_int } +/// Checks if an e-mail address belongs to a known and unblocked contact. +/// +/// Known and unblocked contacts will be returned by +/// [dc_get_contacts]. +/// +/// To validate an e-mail address independently of the contact database +/// use [dc_may_be_valid_addr]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param addr The e-mail-address to check. +/// +/// Returns `1` if address is a contact in use, `0` if address is not +/// a contact in use. #[no_mangle] pub unsafe extern "C" fn dc_lookup_contact_id_by_addr( context: *mut dc_context_t, @@ -805,11 +2141,32 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr( ) -> u32 { assert!(!context.is_null()); assert!(!addr.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); Contact::lookup_id_by_addr(context, as_str(addr)) } +/// Adds a single contact as a result of an _explicit_ user action. +/// +/// We assume, the contact name, if any, is entered by the user and is +/// used "as is" therefore, [normalize] is _not_ called for the +/// name. If the contact is blocked, it is unblocked. +/// +/// To add a number of contacts, see [dc_add_address_book] which is +/// much faster for adding a bunch of addresses. +/// +/// May result in a #DC_EVENT_CONTACTS_CHANGED event. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param name Name of the contact to add. If you do not know the name belonging +/// to the address, you can give NULL here. +/// @param addr E-mail-address of the contact to add. If the email address +/// already exists, the name is updated and the origin is increased to +/// "manually created". +/// +/// Returns a contact ID of the created or reused contact. #[no_mangle] pub unsafe extern "C" fn dc_create_contact( context: *mut dc_context_t, @@ -818,17 +2175,40 @@ pub unsafe extern "C" fn dc_create_contact( ) -> u32 { assert!(!context.is_null()); assert!(!addr.is_null()); - - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let name = if name.is_null() { "" } else { as_str(name) }; - match Contact::create(context, name, as_str(addr)) { Ok(id) => id, Err(_) => 0, } } +/// Adds a number of contacts. +/// +/// Typically used to add the whole address book from the OS. As names +/// here are typically not well formatted, we call normalize() for +/// each name given. +/// +/// No email-address is added twice. Trying to add email-addresses +/// that are already in the contact list, results in updating the name +/// unless the name was changed manually by the user. If any +/// email-address or any name is really updated, the event +/// DC_EVENT_CONTACTS_CHANGED is sent. +/// +/// To add a single contact entered by the user, you should prefer +/// [dc_create_contact], however, for adding a bunch of addresses, +/// this function is _much_ faster. +/// +/// @memberof [dc_context_t] +/// +/// @param context the context object as created by [dc_context_new]. +/// @param adr_book A multi-line string in the format +/// `Name one\nAddress one\nName two\nAddress two`. +/// If an email address already exists, the name is updated +/// unless it was edited manually by [dc_create_contact] before. +/// +/// Returns the number of modified or added contacts. #[no_mangle] pub unsafe extern "C" fn dc_add_address_book( context: *mut dc_context_t, @@ -836,14 +2216,32 @@ pub unsafe extern "C" fn dc_add_address_book( ) -> libc::c_int { assert!(!context.is_null()); assert!(!addr_book.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); match Contact::add_address_book(context, as_str(addr_book)) { Ok(cnt) => cnt as libc::c_int, Err(_) => 0, } } +/// Returns known and unblocked contacts. +/// +/// To get information about a single contact, see [dc_get_contact]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param listflags A combination of flags: +/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the +/// list unless filtered by other parameters +/// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified +/// contacts are returned. if DC_GCL_VERIFIED_ONLY is not set, +/// verified and unverified contacts are returned. +/// @param query A string to filter the list. Typically used to implement an +/// incremental search. NULL for no filtering. +/// +/// Returns an array containing all contact IDs. Must be +/// [dc_array_unref]'d after usage. #[no_mangle] pub unsafe extern "C" fn dc_get_contacts( context: *mut dc_context_t, @@ -851,40 +2249,63 @@ pub unsafe extern "C" fn dc_get_contacts( query: *mut libc::c_char, ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let query = if query.is_null() { None } else { Some(as_str(query)) }; - match Contact::get_all(context, flags, query) { Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))), Err(_) => std::ptr::null_mut(), } } +/// Gets the number of blocked contacts. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// +/// Returns the number of blocked contacts. #[no_mangle] pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); Contact::get_blocked_cnt(context) as libc::c_int } +/// Get blocked contacts. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// +/// Returns an array containing all blocked contact IDs. Must be +/// [dc_array_unref]'d after usage. #[no_mangle] pub unsafe extern "C" fn dc_get_blocked_contacts( context: *mut dc_context_t, ) -> *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); Box::into_raw(Box::new(dc_array_t::from(Contact::get_all_blocked( context, )))) } +/// Block or unblock a contact. +/// +/// May result in a #DC_EVENT_CONTACTS_CHANGED event. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param contact_id The ID of the contact to block or unblock. +/// @param new_blocking 1=block contact, 0=unblock contact #[no_mangle] pub unsafe extern "C" fn dc_block_contact( context: *mut dc_context_t, @@ -892,8 +2313,8 @@ pub unsafe extern "C" fn dc_block_contact( block: libc::c_int, ) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); if block == 0 { Contact::unblock(context, contact_id); } else { @@ -901,14 +2322,26 @@ pub unsafe extern "C" fn dc_block_contact( } } +/// Gets encryption info for a contact. +/// +/// Get a multi-line encryption info, containing your fingerprint and +/// the fingerprint of the contact, used eg. to compare the +/// fingerprints for a simple out-of-band verification. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param contact_id ID of the contact to get the encryption info for. +/// +/// Returns multi-line text, must be free()'d after usage. #[no_mangle] pub unsafe extern "C" fn dc_get_contact_encrinfo( context: *mut dc_context_t, contact_id: u32, ) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); Contact::get_encrinfo(context, contact_id) .map(|s| s.strdup()) .unwrap_or_else(|e| { @@ -917,33 +2350,121 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo( }) } +/// Deletes a contact. +/// +/// The contact is deleted from the local device. It may happen that +/// this is not possible as the contact is in use. In this case, the +/// contact can be blocked. +/// +/// May result in a #DC_EVENT_CONTACTS_CHANGED event. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param contact_id ID of the contact to delete. +/// +/// Returns `1` on success, `0` on error. #[no_mangle] pub unsafe extern "C" fn dc_delete_contact( context: *mut dc_context_t, contact_id: u32, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); match Contact::delete(context, contact_id) { Ok(_) => 1, Err(_) => 0, } } +/// Get a single contact object. +/// +/// For a list, see eg. [dc_get_contacts]. +/// +/// For contact DC_CONTACT_ID_SELF (1), the function returns sth. +/// like "Me" in the selected language and the email address defined +/// by [dc_set_config]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object as created by [dc_context_new]. +/// @param contact_id ID of the contact to get the object for. +/// +/// Returns the contact object, must be freed using [dc_contact_unref] +/// when no longer used. NULL on errors. #[no_mangle] pub unsafe extern "C" fn dc_get_contact<'a>( context: *mut dc_context_t, contact_id: u32, ) -> *mut dc_contact_t<'a> { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); Contact::get_by_id(context, contact_id) .map(|contact| Box::into_raw(Box::new(contact))) .unwrap_or_else(|_| std::ptr::null_mut()) } +/// Import/export things. +/// +/// For this purpose, the function creates a job that is executed in +/// the IMAP-thread then; this requires to call [dc_perform_imap_jobs] +/// regularly. +/// +/// What to do is defined by the _what_ parameter which may be one of +/// the following: +/// +/// - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the +/// directory given as `param1`. The backup contains all contacts, +/// chats, images and other data and device independent settings. +/// The backup does not contain device dependent settings as +/// ringtones or LED notification settings. The name of the backup +/// is typically `delta-chat..bak`, if more than one backup is +/// create on a day, the format is `delta-chat.-.bak` +/// +/// - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: +/// directory) to import. The file is normally created by +/// DC_IMEX_EXPORT_BACKUP and detected by +/// [dc_imex_has_backup]. Importing a backup is only possible as +/// long as the context is not configured or used in another way. +/// +/// - **DC_IMEX_EXPORT_SELF_KEYS** (1) - Export all private keys and +/// all public keys of the user to the directory given as `param1`. +/// The default key is written to the files `public-key-default.asc` +/// and `private-key-default.asc`, if there are more keys, they are +/// written to files as `public-key-.asc` and +/// `private-key-.asc` +/// +/// - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in +/// the directory given as `param1`. The last imported key is made +/// the default keys unless its name contains the string `legacy`. +/// Public keys are not imported. +/// +/// While dc_imex() returns immediately, the started job may take a while, +/// you can stop it using dc_stop_ongoing_process(). During execution of the job, +/// some events are sent out: +/// +/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be +/// used to create a progress bar or stuff like that. Moreover, +/// you'll be informed when the imex-job is done. +/// +/// - For each file written on export, the function sends +/// #DC_EVENT_IMEX_FILE_WRITTEN +/// +/// Only one import-/export-progress can run at the same time. To +/// cancel an import-/export-progress, use [dc_stop_ongoing_process]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param what One of the DC_IMEX_* constants. +/// @param param1 Meaning depends on the DC_IMEX_* constants. If this +/// parameter is a directory, it should not end with a slash +/// (otherwise you'll get double slashes when receiving +/// #DC_EVENT_IMEX_FILE_WRITTEN). Set to NULL if not used. +/// @param param2 Meaning depends on the DC_IMEX_* constants. Set to +/// NULL if not used. #[no_mangle] pub unsafe extern "C" fn dc_imex( context: *mut dc_context_t, @@ -952,11 +2473,62 @@ pub unsafe extern "C" fn dc_imex( param2: *mut libc::c_char, ) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_imex::dc_imex(context, what, param1, param2) } +/// Check if there is a backup file. +/// +/// May only be used on fresh installations (eg. [dc_is_configured] +/// returns 0). +/// +/// # Example +/// +/// ```c +/// char dir[] = "/dir/to/search/backups/in"; +/// +/// void ask_user_for_credentials() +/// { +/// // - ask the user for email and password +/// // - save them using dc_set_config() +/// } +/// +/// int ask_user_whether_to_import() +/// { +/// // - inform the user that we've found a backup +/// // - ask if he want to import it +/// // - return 1 to import, 0 to skip +/// return 1; +/// } +/// +/// if (!dc_is_configured(context)) +/// { +/// char* file = NULL; +/// if ((file=dc_imex_has_backup(context, dir))!=NULL && ask_user_whether_to_import()) +/// { +/// dc_imex(context, DC_IMEX_IMPORT_BACKUP, file, NULL); +/// // connect +/// } +/// else +/// { +/// do { +/// ask_user_for_credentials(); +/// } +/// while (!configure_succeeded()) +/// } +/// free(file); +/// } +/// ``` +/// +/// @memberof [dc_context_t] +/// +/// @param context The context as created by [dc_context_new]. +/// @param dir_name Directory to search backups in. +/// +/// Returns a string with the backup file, typically given to +/// [dc_imex], returned strings must be free()'d. The function +/// returns NULL if no backup was found. #[no_mangle] pub unsafe extern "C" fn dc_imex_has_backup( context: *mut dc_context_t, @@ -964,19 +2536,95 @@ pub unsafe extern "C" fn dc_imex_has_backup( ) -> *mut libc::c_char { assert!(!context.is_null()); assert!(!dir.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_imex::dc_imex_has_backup(context, dir) } +/// Initiate Autocrypt Setup Transfer. +/// +/// Before starting the setup transfer with this function, the user +/// should be asked: +/// +/// ```text + +/// "An 'Autocrypt Setup Message' securely shares your end-to-end +/// setup with other Autocrypt-compliant apps. The setup will be +/// encrypted by a setup code which is displayed here and must be +/// typed on the other device. +/// ``` +/// +/// After that, this function should be called to send the Autocrypt +/// Setup Message. The function creates the setup message and waits +/// until it is really sent. As this may take a while, it is +/// recommended to start the function in a separate thread; to +/// interrupt it, you can use [dc_stop_ongoing_process]. +/// +/// After everything succeeded, the required setup code is returned in +/// the following format: +/// +/// ```text +/// 1234-1234-1234-1234-1234-1234-1234-1234-1234 +/// ``` +/// +/// The setup code should be shown to the user then: +/// +/// ```text +/// Your key has been sent to yourself. Switch to the other device +/// and open the setup message. You should be prompted for a setup +/// code. Type the following digits into the prompt: +/// +/// 1234 - 1234 - 1234 - +/// 1234 - 1234 - 1234 - +/// 1234 - 1234 - 1234 +/// +/// Once you're done, your other device will be ready to use Autocrypt. +/// ``` +/// +/// On the _other device_ you will call [dc_continue_key_transfer] +/// then for setup messages identified by [dc_msg_is_setupmessage]. +/// +/// For more details about the Autocrypt setup process, please refer to +/// https://autocrypt.org/en/latest/level1.html#autocrypt-setup-message +/// +/// @memberof [dc_context_t] +/// @param context The context object. +/// +/// Returns the setup code. Must be free()'d after usage. On errors, +/// eg. if the message could not be sent, NULL is returned. #[no_mangle] pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_imex::dc_initiate_key_transfer(context) } +/// Continue the Autocrypt Key Transfer on another device. +/// +/// If you have started the key transfer on another device using +/// [dc_initiate_key_transfer] and you've detected a setup message +/// with [dc_msg_is_setupmessage], you should prompt the user for the +/// setup code and call this function then. +/// +/// You can use [dc_msg_get_setupcodebegin] to give the user a hint +/// about the code (useful if the user has created several messages +/// and should not enter the wrong code). +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param msg_id ID of the setup message to decrypt. +/// @param setup_code Setup code entered by the user. This is the same +/// setup code as returned from dc_initiate_key_transfer() on the +/// other device. There is no need to format the string +/// correctly, the function will remove all spaces and other +/// characters and insert the `-` characters at the correct +/// places. +/// +/// Returns `1` if the key successfully decrypted and imported; both +/// devices will use the same key now; `0` if key transfer failed +/// eg. due to a bad setup code. #[no_mangle] pub unsafe extern "C" fn dc_continue_key_transfer( context: *mut dc_context_t, @@ -985,19 +2633,73 @@ pub unsafe extern "C" fn dc_continue_key_transfer( ) -> libc::c_int { assert!(!context.is_null()); assert!(!setup_code.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_imex::dc_continue_key_transfer(context, msg_id, setup_code) } +/// Signal an ongoing process to stop. +/// +/// After that, [dc_stop_ongoing_process] returns _without_ waiting +/// for the ongoing process to return. +/// +/// The ongoing process will return ASAP then, however, it may +/// still take a moment. If in doubt, the caller may also decide to kill the +/// thread after a few seconds; eg. the process may hang in a +/// function not under the control of the core (eg. #DC_EVENT_HTTP_GET). Another +/// reason for [dc_stop_ongoing_process] not to wait is that otherwise it +/// would be GUI-blocking and should be started in another thread then; this +/// would make things even more complicated. +/// +/// Typical ongoing processes are started by [dc_configure], +/// [dc_initiate_key_transfer] or [dc_imex]. As there is always at +/// most only one onging process at the same time, there is no need to +/// define _which_ process to exit. +/// +/// @memberof [dc_context_t] +/// +/// # Parameters +/// +/// * `context` - The [dc_context_t] object. +/// +/// # Panics +/// +/// It is safe to call this function on a closed context, which is a +/// no-op. #[no_mangle] pub unsafe extern "C" fn dc_stop_ongoing_process(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - - configure::dc_stop_ongoing_process(context) + let wrapper: &ContextWrapper = &*context; + if wrapper.inner.is_some() { + let context = wrapper.inner.as_ref().expect("context not open"); + configure::dc_stop_ongoing_process(context) + } } +/// Check a scanned QR code. +/// +/// The function should be called after a QR code is scanned. The +/// function takes the raw text scanned and checks what can be done +/// with it. +/// +/// The QR code state is returned in dc_lot_t::state as: +/// +/// - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID +/// - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name +/// - DC_QR_FPR_OK with dc_lot_t::id=Contact ID +/// - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID +/// - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint +/// - DC_QR_ADDR with dc_lot_t::id=Contact ID +/// - DC_QR_TEXT with dc_lot_t::text1=Text +/// - DC_QR_URL with dc_lot_t::text1=URL +/// - DC_QR_ERROR with dc_lot_t::text1=Error string +/// +/// @memberof [dc_context_t] +/// @param context The context object. +/// @param qr The text of the scanned QR code. +/// +/// Returns the parsed QR code as a [dc_lot_t] object. The returned +/// object must be freed using [dc_lot_unref] after usage. #[no_mangle] pub unsafe extern "C" fn dc_check_qr( context: *mut dc_context_t, @@ -1005,23 +2707,63 @@ pub unsafe extern "C" fn dc_check_qr( ) -> *mut dc_lot_t { assert!(!context.is_null()); assert!(!qr.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let lot = qr::check_qr(context, as_str(qr)); Box::into_raw(Box::new(lot)) } +/// Get QR code text that will offer an secure-join verification. +/// +/// The QR code is compatible to the OPENPGP4FPR format so that a +/// basic fingerprint comparison also works eg. with OpenKeychain. +/// +/// The scanning device will pass the scanned content to [dc_check_qr] then; +/// if this function returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP +/// an out-of-band-verification can be joined using [dc_join_securejoin]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param group_chat_id If set to a group-chat-id, +/// the group-join-protocol is offered in the QR code; +/// works for verified groups as well as for normal groups. +/// If set to 0, the setup-Verified-contact-protocol is offered in the QR code. +/// +/// Returns text that should go to the QR code, On errors, an empty QR +/// code is returned, NULL is never returned. The returned string +/// must be free()'d after usage. #[no_mangle] pub unsafe extern "C" fn dc_get_securejoin_qr( context: *mut dc_context_t, chat_id: u32, ) -> *mut libc::c_char { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_securejoin::dc_get_securejoin_qr(context, chat_id) } +/// Join an out-of-band-verification initiated on another device with +/// [dc_get_securejoin_qr]. +/// +/// This function is typically called when dc_check_qr() returns +/// lot.state=DC_QR_ASK_VERIFYCONTACT or +/// lot.state=DC_QR_ASK_VERIFYGROUP. +/// +/// This function takes some time and sends and receives several messages. +/// You should call it in a separate thread; if you want to abort it, you should +/// call [dc_stop_ongoing_process]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object +/// @param qr The text of the scanned QR code. Typically, the same string as given +/// to dc_check_qr(). +/// +/// Returns the chat-id of the joined chat, the UI may redirect to the +/// this chat. If the out-of-band verification failed or was aborted, +/// `0` is returned. #[no_mangle] pub unsafe extern "C" fn dc_join_securejoin( context: *mut dc_context_t, @@ -1029,11 +2771,27 @@ pub unsafe extern "C" fn dc_join_securejoin( ) -> u32 { assert!(!context.is_null()); assert!(!qr.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_securejoin::dc_join_securejoin(context, qr) } +/// Enable or disable location streaming for a chat. +/// +/// Locations are sent to all members of the chat for the given number +/// of seconds; after that, location streaming is automatically +/// disabled for the chat. The current location streaming state of a +/// chat can be checked using [dc_is_sending_locations_to_chat]. +/// +/// The locations that should be sent to the chat can be set using +/// [dc_set_location]. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param chat_id Chat id to enable location streaming for. +/// @param seconds >0: enable location streaming for the given number of seconds; +/// 0: disable location streaming. #[no_mangle] pub unsafe extern "C" fn dc_send_locations_to_chat( context: *mut dc_context_t, @@ -1041,22 +2799,64 @@ pub unsafe extern "C" fn dc_send_locations_to_chat( seconds: libc::c_int, ) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_location::dc_send_locations_to_chat(context, chat_id, seconds as i64) } +/// Check if location streaming is enabled. +/// +/// Location stream can be enabled or disabled using +/// [dc_send_locations_to_chat]. If you have already a [dc_chat_t] +/// object, [dc_chat_is_sending_locations] may be more handy. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param chat_id >0: Check if location streaming is enabled for the given chat. +/// 0: Check of location streaming is enabled for any chat. +/// +/// Returns `1`: location streaming is enabled for the given chat(s); +/// `0`: location streaming is disabled for the given chat(s). #[no_mangle] pub unsafe extern "C" fn dc_is_sending_locations_to_chat( context: *mut dc_context_t, chat_id: u32, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_location::dc_is_sending_locations_to_chat(context, chat_id) as libc::c_int } +/// Sets current location. +/// +/// The location is sent to all chats where location streaming is +/// enabled using [dc_send_locations_to_chat]. +/// +/// Typically results in the event #DC_EVENT_LOCATION_CHANGED with +/// contact_id set to DC_CONTACT_ID_SELF. +/// +/// The UI should call this function on all location changes. The +/// locations set by this function are not sent immediately, instead a +/// message with the last locations is sent out every some minutes or +/// when the user sends out a normal message, the last locations are +/// attached. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param latitude North-south position of the location. +/// Set to 0.0 if the latitude is not known. +/// @param longitude East-west position of the location. +/// Set to 0.0 if the longitude is not known. +/// @param accuracy Estimated accuracy of the location, radial, in meters. +/// Set to 0.0 if the accuracy is not known. +/// +/// Returns `1`: location streaming is still enabled for at least one +/// chat, this dc_set_location() should be called as soon as the +/// location changes; `0`: location streaming is no longer needed, +/// [dc_is_sending_locations_to_chat] is false for all chats. #[no_mangle] pub unsafe extern "C" fn dc_set_location( context: *mut dc_context_t, @@ -1065,11 +2865,72 @@ pub unsafe extern "C" fn dc_set_location( accuracy: libc::c_double, ) -> libc::c_int { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_location::dc_set_location(context, latitude, longitude, accuracy) } +/// Get shared locations from the database. +/// +/// The locations can be filtered by the chat-id, the contact-id and +/// by a timespan. +/// +/// The number of returned locations can be retrieved using +/// [dc_array_get_cnt]. To get information for each location, use +/// [dc_array_get_latitude], [dc_array_get_longitude], +/// [dc_array_get_accuracy], [dc_array_get_timestamp], +/// [dc_array_get_contact_id] and [dc_array_get_msg_id]. The latter +/// returns 0 if there is no message bound to the location. +/// +/// Note that only if [dc_array_is_independent] returns 0, the +/// location is the current or a past position of the user. If +/// [dc_array_is_independent] returns 1, the location is any location +/// on earth that is marked by the user. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. +/// @param chat_id Chat-id to get location information for. +/// 0 to get locations independently of the chat. +/// @param contact_id Contact-id to get location information for. +/// If also a chat-id is given, this should be a member of the given chat. +/// 0 to get locations independently of the contact. +/// @param timestamp_from Start of timespan to return. +/// Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC. +/// 0 for "start from the beginning". +/// @param timestamp_to End of timespan to return. +/// Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC. +/// 0 for "all up to now". +/// +/// Returns an array of locations, NULL is never returned. The array +/// is sorted decending; the first entry in the array is the location +/// with the newest timestamp. Note that this is only realated to the +/// recent postion of the user if dc_array_is_independent() returns +/// `0`. The returned array must be freed using [dc_array_unref]. +/// +/// # Example +/// +/// ```c +/// // get locations from the last hour for a global map +/// dc_array_t* loc = dc_get_locations(context, 0, 0, time(NULL)-60*60, 0); +/// for (int i=0; i *mut dc_array::dc_array_t { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let res = dc_location::dc_get_locations( context, chat_id, @@ -1091,11 +2952,21 @@ pub unsafe extern "C" fn dc_get_locations( Box::into_raw(Box::new(dc_array_t::from(res))) } +/// Delete all locations on the current device. +/// +/// Locations already sent cannot be deleted. +/// +/// Typically results in the event #DC_EVENT_LOCATION_CHANGED with +/// contact_id set to 0. +/// +/// @memberof [dc_context_t] +/// +/// @param context The context object. #[no_mangle] pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) { assert!(!context.is_null()); - let context = &*context; - + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); dc_location::dc_delete_all_locations(context); } @@ -1310,7 +3181,10 @@ pub unsafe extern "C" fn dc_chatlist_get_context( assert!(!chatlist.is_null()); let list = &*chatlist; - list.get_context() as *const _ + let context: &Context = list.get_context(); + let userdata_ptr = context.userdata as *const ContextWrapper; + let wrapper: &ContextWrapper = &*userdata_ptr; + wrapper } // dc_chat_t @@ -1427,16 +3301,15 @@ pub unsafe extern "C" fn dc_msg_new<'a>( viewtype: libc::c_int, ) -> *mut dc_msg_t<'a> { assert!(!context.is_null()); - let context = &*context; + let wrapper: &ContextWrapper = &*context; + let context = wrapper.inner.as_ref().expect("context not open"); let viewtype = from_prim(viewtype).expect(&format!("invalid viewtype = {}", viewtype)); - Box::into_raw(Box::new(message::dc_msg_new(context, viewtype))) } #[no_mangle] pub unsafe extern "C" fn dc_msg_unref(msg: *mut dc_msg_t) { assert!(!msg.is_null()); - Box::from_raw(msg); } diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index cbb06289b..2d6c76e56 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -396,8 +396,6 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E _ => println!( "==========================Database commands==\n\ info\n\ - open \n\ - close\n\ set []\n\ get \n\ oauth2\n\ @@ -474,14 +472,6 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E println!("Already authorized."); } } - "open" => { - ensure!(!arg1.is_empty(), "Argument missing"); - dc_close(context); - ensure!(dc_open(context, arg1, None), "Open failed"); - } - "close" => { - dc_close(context); - } "initiate-key-transfer" => { let setup_code = dc_initiate_key_transfer(context); if !setup_code.is_null() { diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 5b5409dd8..37408d506 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -14,6 +14,7 @@ extern crate lazy_static; extern crate rusqlite; use std::borrow::Cow::{self, Borrowed, Owned}; +use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; @@ -41,7 +42,7 @@ use self::cmdline::*; // Event Handler -unsafe extern "C" fn receive_event( +fn receive_event( _context: &Context, event: Event, data1: uintptr_t, @@ -384,21 +385,16 @@ impl Highlighter for DcHelper { impl Helper for DcHelper {} fn main_0(args: Vec) -> Result<(), failure::Error> { - let mut context = dc_context_new( - Some(receive_event), - 0 as *mut libc::c_void, - Some("CLI".into()), - ); - unsafe { dc_cmdline_skip_auth() }; - - if args.len() == 2 { - if unsafe { !dc_open(&mut context, &args[1], None) } { - println!("Error: Cannot open {}.", args[0],); - } - } else if args.len() != 1 { + if args.len() != 2 { println!("Error: Bad arguments, expected [db-name]."); + return Err(format_err!("No db-name specified")); } + let context = Context::new( + Box::new(receive_event), + "CLI".into(), + Path::new(&args[1]).to_path_buf(), + )?; println!("Delta Chat Core is awaiting your commands."); diff --git a/examples/simple.rs b/examples/simple.rs index e5d31ea06..efb5370ca 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -16,9 +16,8 @@ use deltachat::job::{ perform_imap_fetch, perform_imap_idle, perform_imap_jobs, perform_smtp_idle, perform_smtp_jobs, }; -extern "C" fn cb(_ctx: &Context, event: Event, data1: usize, data2: usize) -> usize { +fn cb(_ctx: &Context, event: Event, data1: usize, data2: usize) -> usize { println!("[{:?}]", event); - match event { Event::CONFIGURE_PROGRESS => { println!(" progress: {}", data1); @@ -39,7 +38,11 @@ extern "C" fn cb(_ctx: &Context, event: Event, data1: usize, data2: usize) -> us fn main() { unsafe { - let ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None); + let dir = tempdir().unwrap(); + let dbfile = dir.path().join("db.sqlite"); + println!("creating database {:?}", dbfile); + let ctx = + Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context"); let running = Arc::new(RwLock::new(true)); let info = dc_get_info(&ctx); let info_s = CStr::from_ptr(info); @@ -73,13 +76,6 @@ fn main() { } }); - let dir = tempdir().unwrap(); - let dbfile = dir.path().join("db.sqlite"); - - println!("opening database {:?}", dbfile); - - assert!(dc_open(&ctx, dbfile.to_str().unwrap(), None)); - println!("configuring"); let args = std::env::args().collect::>(); assert_eq!(args.len(), 2, "missing password"); diff --git a/python/tests/test_lowlevel.py b/python/tests/test_lowlevel.py index d0061afef..0e92d1af0 100644 --- a/python/tests/test_lowlevel.py +++ b/python/tests/test_lowlevel.py @@ -17,11 +17,19 @@ def test_callback_None2int(): clear_context_callback(ctx) -def test_dc_close_events(): - ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL) +def test_dc_close_events(tmpdir): + ctx = ffi.gc( + capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL), + lib.dc_context_unref, + ) evlog = EventLogger(ctx) evlog.set_timeout(5) - set_context_callback(ctx, lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)) + set_context_callback( + ctx, + lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2), + ) + p = tmpdir.join("hello.db") + lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL) capi.lib.dc_close(ctx) def find(info_string): diff --git a/src/context.rs b/src/context.rs index 45f3c5de4..96b0813a5 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,3 +1,5 @@ +use std::ffi::OsString; +use std::path::Path; use std::sync::{Arc, Condvar, Mutex, RwLock}; use crate::chat::*; @@ -7,6 +9,7 @@ use crate::dc_loginparam::*; use crate::dc_move::*; use crate::dc_receive_imf::*; use crate::dc_tools::*; +use crate::error::*; use crate::imap::*; use crate::job::*; use crate::job_thread::JobThread; @@ -22,6 +25,15 @@ use std::ptr; use std::path::PathBuf; +/// Callback function type for [Context]. +/// +/// @param context The context object as returned by dc_context_new(). +/// @param event one of the @ref DC_EVENT constants +/// @param data1 depends on the event parameter +/// @param data2 depends on the event parameter +/// @return return 0 unless stated otherwise in the event parameter documentation +pub type ContextCallback = dyn Fn(&Context, Event, uintptr_t, uintptr_t) -> uintptr_t; + pub struct Context { pub userdata: *mut libc::c_void, pub dbfile: Arc>>, @@ -35,7 +47,7 @@ pub struct Context { pub smtp: Arc>, pub smtp_state: Arc<(Mutex, Condvar)>, pub oauth2_critical: Arc>, - pub cb: Option, + pub cb: Box, pub os_name: Option, pub cmdline_sel_chat_id: Arc>, pub bob: Arc>, @@ -55,6 +67,76 @@ pub struct RunningState { } impl Context { + pub fn new(cb: Box, os_name: String, dbfile: PathBuf) -> Result { + let mut blob_fname = OsString::new(); + blob_fname.push(dbfile.file_name().unwrap_or_default()); + blob_fname.push("-blobs"); + let blobdir = dbfile.with_file_name(blob_fname); + if !blobdir.exists() { + std::fs::create_dir_all(&blobdir)?; + } + Context::with_blobdir(cb, os_name, dbfile, &blobdir) + } + + pub fn with_blobdir( + cb: Box, + os_name: String, + dbfile: PathBuf, + blobdir: &Path, + ) -> Result { + if !blobdir.is_dir() { + return Err(format_err!("Blobdir does not exist: {}", blobdir.display())); + } + let ctx = Context { + blobdir: Arc::new(RwLock::new(unsafe { blobdir.strdup() })), + dbfile: Arc::new(RwLock::new(Some(dbfile))), + inbox: Arc::new(RwLock::new({ + Imap::new( + cb_get_config, + cb_set_config, + cb_precheck_imf, + cb_receive_imf, + ) + })), + userdata: std::ptr::null_mut(), + cb, + os_name: Some(os_name), + running_state: Arc::new(RwLock::new(Default::default())), + sql: Sql::new(), + smtp: Arc::new(Mutex::new(Smtp::new())), + smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())), + oauth2_critical: Arc::new(Mutex::new(())), + bob: Arc::new(RwLock::new(Default::default())), + last_smeared_timestamp: Arc::new(RwLock::new(0)), + cmdline_sel_chat_id: Arc::new(RwLock::new(0)), + sentbox_thread: Arc::new(RwLock::new(JobThread::new( + "SENTBOX", + "configured_sentbox_folder", + Imap::new( + cb_get_config, + cb_set_config, + cb_precheck_imf, + cb_receive_imf, + ), + ))), + mvbox_thread: Arc::new(RwLock::new(JobThread::new( + "MVBOX", + "configured_mvbox_folder", + Imap::new( + cb_get_config, + cb_set_config, + cb_precheck_imf, + cb_receive_imf, + ), + ))), + probe_imap_network: Arc::new(RwLock::new(false)), + perform_inbox_jobs_needed: Arc::new(RwLock::new(false)), + generating_key_mutex: Mutex::new(()), + }; + ctx.sql.open(&ctx, &ctx.dbfile.read().unwrap().as_ref().unwrap(), 0)?; + Ok(ctx) + } + pub fn has_dbfile(&self) -> bool { self.get_dbfile().is_some() } @@ -74,11 +156,7 @@ impl Context { } pub fn call_cb(&self, event: Event, data1: uintptr_t, data2: uintptr_t) -> uintptr_t { - if let Some(cb) = self.cb { - unsafe { cb(self, event, data1, data2) } - } else { - 0 - } + (*self.cb)(self, event, data1, data2) } } @@ -115,60 +193,6 @@ pub struct SmtpState { pub probe_network: bool, } -// create/open/config/information -pub fn dc_context_new( - cb: Option, - userdata: *mut libc::c_void, - os_name: Option, -) -> Context { - Context { - blobdir: Arc::new(RwLock::new(std::ptr::null_mut())), - dbfile: Arc::new(RwLock::new(None)), - inbox: Arc::new(RwLock::new({ - Imap::new( - cb_get_config, - cb_set_config, - cb_precheck_imf, - cb_receive_imf, - ) - })), - userdata, - cb, - os_name, - running_state: Arc::new(RwLock::new(Default::default())), - sql: Sql::new(), - smtp: Arc::new(Mutex::new(Smtp::new())), - smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())), - oauth2_critical: Arc::new(Mutex::new(())), - bob: Arc::new(RwLock::new(Default::default())), - last_smeared_timestamp: Arc::new(RwLock::new(0)), - cmdline_sel_chat_id: Arc::new(RwLock::new(0)), - sentbox_thread: Arc::new(RwLock::new(JobThread::new( - "SENTBOX", - "configured_sentbox_folder", - Imap::new( - cb_get_config, - cb_set_config, - cb_precheck_imf, - cb_receive_imf, - ), - ))), - mvbox_thread: Arc::new(RwLock::new(JobThread::new( - "MVBOX", - "configured_mvbox_folder", - Imap::new( - cb_get_config, - cb_set_config, - cb_precheck_imf, - cb_receive_imf, - ), - ))), - probe_imap_network: Arc::new(RwLock::new(false)), - perform_inbox_jobs_needed: Arc::new(RwLock::new(false)), - generating_key_mutex: Mutex::new(()), - } -} - unsafe fn cb_receive_imf( context: &Context, imf_raw_not_terminated: *const libc::c_char, @@ -287,39 +311,10 @@ pub unsafe fn dc_close(context: &Context) { *blobdir = ptr::null_mut(); } -pub unsafe fn dc_is_open(context: &Context) -> libc::c_int { - context.sql.is_open() as libc::c_int -} - pub unsafe fn dc_get_userdata(context: &mut Context) -> *mut libc::c_void { context.userdata as *mut _ } -pub unsafe fn dc_open(context: &Context, dbfile: &str, blobdir: Option<&str>) -> bool { - let mut success = false; - if 0 != dc_is_open(context) { - return false; - } - *context.dbfile.write().unwrap() = Some(PathBuf::from(dbfile)); - if blobdir.is_some() && !blobdir.unwrap().is_empty() { - let dir = dc_ensure_no_slash_safe(blobdir.unwrap()).strdup(); - *context.blobdir.write().unwrap() = dir; - } else { - let dir = dbfile.to_string() + "-blobs"; - dc_create_folder(context, &dir); - *context.blobdir.write().unwrap() = dir.strdup(); - } - // Create/open sqlite database, this may already use the blobdir - let dbfile_path = std::path::Path::new(dbfile); - if context.sql.open(context, dbfile_path, 0) { - success = true - } - if !success { - dc_close(context); - } - success -} - pub unsafe fn dc_get_blobdir(context: &Context) -> *mut libc::c_char { dc_strdup(*context.blobdir.clone().read().unwrap()) } @@ -577,19 +572,70 @@ pub fn dc_is_mvbox(context: &Context, folder_name: impl AsRef) -> bool { mod tests { use super::*; + use crate::test_utils::*; + + #[test] + fn test_wrong_db() { + let tmp = tempfile::tempdir().unwrap(); + let dbfile = tmp.path().join("db.sqlite"); + std::fs::write(&dbfile, b"123").unwrap(); + let res = Context::new(Box::new(|_, _, _, _| 0), "FakeOs".into(), dbfile); + assert!(res.is_err()); + } + + #[test] + fn test_blobdir_exists() { + let tmp = tempfile::tempdir().unwrap(); + let dbfile = tmp.path().join("db.sqlite"); + Context::new(Box::new(|_, _, _, _| 0), "FakeOS".into(), dbfile).unwrap(); + let blobdir = tmp.path().join("db.sqlite-blobs"); + assert!(blobdir.is_dir()); + } + + #[test] + fn test_wrong_blogdir() { + let tmp = tempfile::tempdir().unwrap(); + let dbfile = tmp.path().join("db.sqlite"); + let blobdir = tmp.path().join("db.sqlite-blobs"); + std::fs::write(&blobdir, b"123").unwrap(); + let res = Context::new(Box::new(|_, _, _, _| 0), "FakeOS".into(), dbfile); + assert!(res.is_err()); + } + + #[test] + fn test_sqlite_parent_not_exists() { + let tmp = tempfile::tempdir().unwrap(); + let subdir = tmp.path().join("subdir"); + let dbfile = subdir.join("db.sqlite"); + let dbfile2 = dbfile.clone(); + Context::new(Box::new(|_, _, _, _| 0), "FakeOS".into(), dbfile).unwrap(); + assert!(subdir.is_dir()); + assert!(dbfile2.is_file()); + } + + #[test] + fn test_with_blobdir_not_exists() { + let tmp = tempfile::tempdir().unwrap(); + let dbfile = tmp.path().join("db.sqlite"); + let blobdir = tmp.path().join("blobs"); + let res = + Context::with_blobdir(Box::new(|_, _, _, _| 0), "FakeOS".into(), dbfile, &blobdir); + assert!(res.is_err()); + } + #[test] fn no_crashes_on_context_deref() { - let ctx = dc_context_new(None, std::ptr::null_mut(), Some("Test OS".into())); - std::mem::drop(ctx); + let t = dummy_context(); + std::mem::drop(t.ctx); } #[test] fn test_context_double_close() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); + let t = dummy_context(); unsafe { - dc_close(&ctx); - dc_close(&ctx); + dc_close(&t.ctx); + dc_close(&t.ctx); } - std::mem::drop(ctx); + std::mem::drop(t.ctx); } } diff --git a/src/dc_imex.rs b/src/dc_imex.rs index f1f440f69..56594c162 100644 --- a/src/dc_imex.rs +++ b/src/dc_imex.rs @@ -74,7 +74,7 @@ pub unsafe fn dc_imex_has_backup( let name = name.to_string_lossy(); if name.starts_with("delta-chat") && name.ends_with(".bak") { let sql = Sql::new(); - if sql.open(context, &path, 0x1) { + if sql.open(context, &path, 0x1).is_ok() { let curr_backup_time = sql.get_config_int(context, "backup_time") .unwrap_or_default() as u64; @@ -617,9 +617,10 @@ unsafe fn import_backup(context: &Context, backup_to_import: *const libc::c_char } /* error already logged */ /* re-open copied database file */ - if !context + if context .sql .open(&context, &context.get_dbfile().unwrap(), 0) + .is_err() { return 0; } @@ -710,7 +711,7 @@ unsafe fn import_backup(context: &Context, backup_to_import: *const libc::c_char /* the FILE_PROGRESS macro calls the callback with the permille of files processed. The macro avoids weird values of 0% or 100% while still working. */ // TODO should return bool /rtn -#[allow(non_snake_case)] +#[allow(non_snake_case, unused_must_use)] unsafe fn export_backup(context: &Context, dir: *const libc::c_char) -> libc::c_int { let mut current_block: u64; let mut success: libc::c_int = 0; @@ -757,7 +758,7 @@ unsafe fn export_backup(context: &Context, dir: *const libc::c_char) -> libc::c_ /* 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) */ /*for logging only*/ let sql = Sql::new(); - if sql.open(context, as_path(dest_pathNfilename), 0) { + if sql.open(context, as_path(dest_pathNfilename), 0).is_ok() { if !sql.table_exists("backup_blobs") { if sql::execute( context, @@ -1159,7 +1160,7 @@ mod tests { #[test] fn test_render_setup_file() { - let t = test_context(Some(logging_cb)); + let t = test_context(Some(Box::new(logging_cb))); configure_alice_keypair(&t.ctx); let msg = dc_render_setup_file(&t.ctx, "hello").unwrap(); @@ -1177,14 +1178,9 @@ mod tests { assert!(msg.contains("-----END PGP MESSAGE-----\n")); } - unsafe extern "C" fn ac_setup_msg_cb( - ctx: &Context, - evt: Event, - d1: uintptr_t, - d2: uintptr_t, - ) -> uintptr_t { + fn ac_setup_msg_cb(ctx: &Context, evt: Event, d1: uintptr_t, d2: uintptr_t) -> uintptr_t { if evt == Event::GET_STRING && d1 == StockMessage::AcSetupMsgBody.to_usize().unwrap() { - "hello\r\nthere".strdup() as usize + unsafe { "hello\r\nthere".strdup() as usize } } else { logging_cb(ctx, evt, d1, d2) } @@ -1192,7 +1188,7 @@ mod tests { #[test] fn test_render_setup_file_newline_replace() { - let t = test_context(Some(ac_setup_msg_cb)); + let t = test_context(Some(Box::new(ac_setup_msg_cb))); configure_alice_keypair(&t.ctx); let msg = dc_render_setup_file(&t.ctx, "pw").unwrap(); println!("{}", &msg); diff --git a/src/dc_tools.rs b/src/dc_tools.rs index b500fcc55..bd50dd05e 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -8,14 +8,13 @@ use std::{fmt, fs, ptr}; use chrono::{Local, TimeZone}; use mmime::mailimf_types::*; use rand::{thread_rng, Rng}; +use itertools::max; use crate::context::Context; use crate::error::Error; use crate::types::*; use crate::x::*; -use itertools::max; - /* Some tools and enhancements to the used libraries, there should be no references to Context and other "larger" classes here. */ /* ** library-private **********************************************************/ @@ -1228,7 +1227,7 @@ impl CStringExt for CString {} /// Rust strings to raw C strings. This can be clumsy to do correctly /// and the compiler sometimes allows it in an unsafe way. These /// methods make it more succinct and help you get it right. -pub trait StrExt { +pub trait Strdup { /// Allocate a new raw C `*char` version of this string. /// /// This allocates a new raw C string which must be freed using @@ -1245,13 +1244,26 @@ pub trait StrExt { unsafe fn strdup(&self) -> *mut libc::c_char; } -impl> StrExt for T { +impl> Strdup for T { unsafe fn strdup(&self) -> *mut libc::c_char { let tmp = CString::yolo(self.as_ref()); dc_strdup(tmp.as_ptr()) } } +impl Strdup for CStr { + unsafe fn strdup(&self) -> *mut libc::c_char { + dc_strdup(self.as_ptr()) + } +} + +impl Strdup for std::path::Path { + unsafe fn strdup(&self) -> *mut libc::c_char { + let tmp = self.to_c_string().unwrap(); + dc_strdup(tmp.as_ptr()) + } +} + pub fn to_string(s: *const libc::c_char) -> String { if s.is_null() { return "".into(); diff --git a/src/sql.rs b/src/sql.rs index e42a8ae09..f160576a3 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -38,16 +38,13 @@ impl Sql { info!(context, 0, "Database closed."); } - // return true on success, false on failure - pub fn open(&self, context: &Context, dbfile: &std::path::Path, flags: libc::c_int) -> bool { - match open(context, self, dbfile, flags) { - Ok(_) => true, - Err(Error::SqlAlreadyOpen) => false, - Err(_) => { - self.close(context); - false - } - } + pub fn open( + &self, + context: &Context, + dbfile: &std::path::Path, + flags: libc::c_int, + ) -> Result<()> { + open(context, self, dbfile, flags) } pub fn execute

(&self, sql: &str, params: P) -> Result @@ -227,7 +224,10 @@ impl Sql { match res { Ok(_) => Ok(()), Err(err) => { - error!(context, 0, "set_config(): Cannot change value. {:?}", &err); + error!( + context, + 0, "set_config(): Cannot change value for {}: {:?}", key, &err + ); Err(err.into()) } } diff --git a/src/stock.rs b/src/stock.rs index 20231712b..be64ab80b 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -232,10 +232,7 @@ mod tests { use super::*; use crate::test_utils::*; - use std::ffi::CString; - use crate::constants::DC_CONTACT_ID_SELF; - use crate::context::dc_context_new; use crate::types::uintptr_t; use num_traits::ToPrimitive; @@ -253,19 +250,18 @@ mod tests { #[test] fn test_stock_str() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); - assert_eq!(ctx.stock_str(StockMessage::NoMessages), "No messages."); + let t = dummy_context(); + assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "No messages."); } - unsafe extern "C" fn test_stock_str_no_fallback_cb( + fn test_stock_str_no_fallback_cb( _ctx: &Context, evt: Event, d1: uintptr_t, _d2: uintptr_t, ) -> uintptr_t { if evt == Event::GET_STRING && d1 == StockMessage::NoMessages.to_usize().unwrap() { - let tmp = CString::new("Hello there").unwrap(); - dc_strdup(tmp.as_ptr()) as usize + unsafe { "Hello there".strdup() as usize } } else { 0 } @@ -273,16 +269,16 @@ mod tests { #[test] fn test_stock_str_no_fallback() { - let t = test_context(Some(test_stock_str_no_fallback_cb)); + let t = test_context(Some(Box::new(test_stock_str_no_fallback_cb))); assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "Hello there"); } #[test] fn test_stock_string_repl_str() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); + let t = dummy_context(); // uses %1$s substitution assert_eq!( - ctx.stock_string_repl_str(StockMessage::Member, "42"), + t.ctx.stock_string_repl_str(StockMessage::Member, "42"), "42 member(s)" ); // We have no string using %1$d to test... @@ -290,36 +286,38 @@ mod tests { #[test] fn test_stock_string_repl_int() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); + let t = dummy_context(); assert_eq!( - ctx.stock_string_repl_int(StockMessage::Member, 42), + t.ctx.stock_string_repl_int(StockMessage::Member, 42), "42 member(s)" ); } #[test] fn test_stock_string_repl_str2() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); + let t = dummy_context(); assert_eq!( - ctx.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar"), + t.ctx + .stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar"), "Response from foo: bar" ); } #[test] fn test_stock_system_msg_simple() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); + let t = dummy_context(); assert_eq!( - ctx.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0), + t.ctx + .stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0), "Location streaming enabled." ) } #[test] fn test_stock_system_msg_add_member_by_me() { - let ctx = dc_context_new(None, std::ptr::null_mut(), None); + let t = dummy_context(); assert_eq!( - ctx.stock_system_msg( + t.ctx.stock_system_msg( StockMessage::MsgAddMember, "alice@example.com", "", diff --git a/src/test_utils.rs b/src/test_utils.rs index 1749b81ba..c6d6caaf4 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -9,9 +9,8 @@ use tempfile::{tempdir, TempDir}; use crate::config::Config; use crate::constants::{Event, KeyType}; -use crate::context::{dc_context_new, dc_open, Context}; +use crate::context::{Context, ContextCallback}; use crate::key; -use crate::types::dc_callback_t; /// A Context and temporary directory. /// @@ -28,18 +27,15 @@ pub struct TestContext { /// "db.sqlite" in the [TestContext.dir] directory. /// /// [Context]: crate::context::Context -pub fn test_context(cb: Option) -> TestContext { - unsafe { - let mut ctx = dc_context_new(cb, std::ptr::null_mut(), None); - let dir = tempdir().unwrap(); - let dbfile = dir.path().join("db.sqlite"); - assert!( - dc_open(&mut ctx, dbfile.to_str().unwrap(), None), - "Failed to open {}", - dbfile.display(), - ); - TestContext { ctx: ctx, dir: dir } - } +pub fn test_context(callback: Option>) -> TestContext { + let dir = tempdir().unwrap(); + let dbfile = dir.path().join("db.sqlite"); + let cb: Box = match callback { + Some(cb) => cb, + None => Box::new(|_, _, _, _| 0), + }; + let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap(); + TestContext { ctx: ctx, dir: dir } } /// Return a dummy [TestContext]. @@ -51,13 +47,8 @@ pub fn dummy_context() -> TestContext { test_context(None) } -pub unsafe extern "C" fn logging_cb( - _ctx: &Context, - evt: Event, - _d1: uintptr_t, - d2: uintptr_t, -) -> uintptr_t { - let to_str = |x| CStr::from_ptr(x as *const libc::c_char).to_str().unwrap(); +pub fn logging_cb(_ctx: &Context, evt: Event, _d1: uintptr_t, d2: uintptr_t) -> uintptr_t { + let to_str = unsafe { |x| CStr::from_ptr(x as *const libc::c_char).to_str().unwrap() }; match evt { Event::INFO => println!("I: {}", to_str(d2)), Event::WARNING => println!("W: {}", to_str(d2)), diff --git a/src/types.rs b/src/types.rs index 41f9bb50f..94ab0d072 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,21 +1,9 @@ #![allow(non_camel_case_types)] -use crate::constants::Event; use crate::context::Context; pub use mmime::clist::*; pub use rusqlite::ffi::*; -/// Callback function that should be given to dc_context_new(). -/// -/// @memberof Context -/// @param context The context object as returned by dc_context_new(). -/// @param event one of the @ref DC_EVENT constants -/// @param data1 depends on the event parameter -/// @param data2 depends on the event parameter -/// @return return 0 unless stated otherwise in the event parameter documentation -pub type dc_callback_t = - unsafe extern "C" fn(_: &Context, _: Event, _: uintptr_t, _: uintptr_t) -> uintptr_t; - pub type dc_receive_imf_t = unsafe fn( _: &Context, _: *const libc::c_char, diff --git a/tests/stress.rs b/tests/stress.rs index 018f98155..e0e2b6554 100644 --- a/tests/stress.rs +++ b/tests/stress.rs @@ -33,103 +33,100 @@ static mut S_EM_SETUPFILE: *const libc::c_char = as *const u8 as *const libc::c_char; unsafe fn stress_functions(context: &Context) { - if 0 != dc_is_open(context) { - if dc_file_exist(context, "$BLOBDIR/foobar") - || dc_file_exist(context, "$BLOBDIR/dada") - || dc_file_exist(context, "$BLOBDIR/foobar.dadada") - || dc_file_exist(context, "$BLOBDIR/foobar-folder") - { - dc_delete_file(context, "$BLOBDIR/foobar"); - dc_delete_file(context, "$BLOBDIR/dada"); - dc_delete_file(context, "$BLOBDIR/foobar.dadada"); - dc_delete_file(context, "$BLOBDIR/foobar-folder"); - } - dc_write_file( - context, - b"$BLOBDIR/foobar\x00" as *const u8 as *const libc::c_char, - b"content\x00" as *const u8 as *const libc::c_char as *const libc::c_void, - 7i32 as size_t, - ); - assert!(dc_file_exist(context, "$BLOBDIR/foobar",)); - assert!(!dc_file_exist(context, "$BLOBDIR/foobarx")); - assert_eq!( - dc_get_filebytes(context, "$BLOBDIR/foobar",), - 7i32 as libc::c_ulonglong - ); - - let abs_path: *mut libc::c_char = dc_mprintf( - b"%s/%s\x00" as *const u8 as *const libc::c_char, - context.get_blobdir(), - b"foobar\x00" as *const u8 as *const libc::c_char, - ); - assert!(dc_is_blobdir_path(context, as_str(abs_path))); - assert!(dc_is_blobdir_path(context, "$BLOBDIR/fofo",)); - assert!(!dc_is_blobdir_path(context, "/BLOBDIR/fofo",)); - assert!(dc_file_exist(context, as_path(abs_path))); - free(abs_path as *mut libc::c_void); - assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",)); - assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7); - - let mut buf: *mut libc::c_void = 0 as *mut libc::c_void; - let mut buf_bytes: size_t = 0; - - assert_eq!( - dc_read_file( - context, - b"$BLOBDIR/dada\x00" as *const u8 as *const libc::c_char, - &mut buf, - &mut buf_bytes, - ), - 1 - ); - assert_eq!(buf_bytes, 7); - assert_eq!( - std::str::from_utf8(std::slice::from_raw_parts(buf as *const u8, buf_bytes)).unwrap(), - "content" - ); - - free(buf as *mut _); - assert!(dc_delete_file(context, "$BLOBDIR/foobar")); - assert!(dc_delete_file(context, "$BLOBDIR/dada")); - assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")); - assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",)); - assert!(dc_delete_file(context, "$BLOBDIR/foobar-folder")); - let fn0: *mut libc::c_char = dc_get_fine_pathNfilename( - context, - b"$BLOBDIR\x00" as *const u8 as *const libc::c_char, - b"foobar.dadada\x00" as *const u8 as *const libc::c_char, - ); - assert!(!fn0.is_null()); - assert_eq!( - strcmp( - fn0, - b"$BLOBDIR/foobar.dadada\x00" as *const u8 as *const libc::c_char, - ), - 0 - ); - dc_write_file( - context, - fn0, - b"content\x00" as *const u8 as *const libc::c_char as *const libc::c_void, - 7i32 as size_t, - ); - let fn1: *mut libc::c_char = dc_get_fine_pathNfilename( - context, - b"$BLOBDIR\x00" as *const u8 as *const libc::c_char, - b"foobar.dadada\x00" as *const u8 as *const libc::c_char, - ); - assert!(!fn1.is_null()); - assert_eq!( - strcmp( - fn1, - b"$BLOBDIR/foobar-1.dadada\x00" as *const u8 as *const libc::c_char, - ), - 0 - ); - assert!(dc_delete_file(context, as_path(fn0))); - free(fn0 as *mut libc::c_void); - free(fn1 as *mut libc::c_void); + if dc_file_exist(context, "$BLOBDIR/foobar") + || dc_file_exist(context, "$BLOBDIR/dada") + || dc_file_exist(context, "$BLOBDIR/foobar.dadada") + || dc_file_exist(context, "$BLOBDIR/foobar-folder") + { + dc_delete_file(context, "$BLOBDIR/foobar"); + dc_delete_file(context, "$BLOBDIR/dada"); + dc_delete_file(context, "$BLOBDIR/foobar.dadada"); + dc_delete_file(context, "$BLOBDIR/foobar-folder"); } + dc_write_file( + context, + b"$BLOBDIR/foobar\x00" as *const u8 as *const libc::c_char, + b"content\x00" as *const u8 as *const libc::c_char as *const libc::c_void, + 7i32 as size_t, + ); + assert!(dc_file_exist(context, "$BLOBDIR/foobar",)); + assert!(!dc_file_exist(context, "$BLOBDIR/foobarx")); + assert_eq!( + dc_get_filebytes(context, "$BLOBDIR/foobar",), + 7i32 as libc::c_ulonglong + ); + let abs_path: *mut libc::c_char = dc_mprintf( + b"%s/%s\x00" as *const u8 as *const libc::c_char, + context.get_blobdir(), + b"foobar\x00" as *const u8 as *const libc::c_char, + ); + assert!(dc_is_blobdir_path(context, as_str(abs_path))); + assert!(dc_is_blobdir_path(context, "$BLOBDIR/fofo")); + assert!(!dc_is_blobdir_path(context, "/BLOBDIR/fofo")); + assert!(dc_file_exist(context, as_path(abs_path))); + free(abs_path as *mut libc::c_void); + assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",)); + assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7); + + let mut buf: *mut libc::c_void = 0 as *mut libc::c_void; + let mut buf_bytes: size_t = 0; + + assert_eq!( + dc_read_file( + context, + b"$BLOBDIR/dada\x00" as *const u8 as *const libc::c_char, + &mut buf, + &mut buf_bytes, + ), + 1 + ); + assert_eq!(buf_bytes, 7); + assert_eq!( + std::str::from_utf8(std::slice::from_raw_parts(buf as *const u8, buf_bytes)).unwrap(), + "content" + ); + + free(buf as *mut _); + assert!(dc_delete_file(context, "$BLOBDIR/foobar")); + assert!(dc_delete_file(context, "$BLOBDIR/dada")); + assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")); + assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",)); + assert!(dc_delete_file(context, "$BLOBDIR/foobar-folder")); + let fn0: *mut libc::c_char = dc_get_fine_pathNfilename( + context, + b"$BLOBDIR\x00" as *const u8 as *const libc::c_char, + b"foobar.dadada\x00" as *const u8 as *const libc::c_char, + ); + assert!(!fn0.is_null()); + assert_eq!( + strcmp( + fn0, + b"$BLOBDIR/foobar.dadada\x00" as *const u8 as *const libc::c_char, + ), + 0 + ); + dc_write_file( + context, + fn0, + b"content\x00" as *const u8 as *const libc::c_char as *const libc::c_void, + 7i32 as size_t, + ); + let fn1: *mut libc::c_char = dc_get_fine_pathNfilename( + context, + b"$BLOBDIR\x00" as *const u8 as *const libc::c_char, + b"foobar.dadada\x00" as *const u8 as *const libc::c_char, + ); + assert!(!fn1.is_null()); + assert_eq!( + strcmp( + fn1, + b"$BLOBDIR/foobar-1.dadada\x00" as *const u8 as *const libc::c_char, + ), + 0 + ); + assert!(dc_delete_file(context, as_path(fn0))); + free(fn0 as *mut libc::c_void); + free(fn1 as *mut libc::c_void); let res = context.get_config(config::Config::SysConfigKeys).unwrap(); @@ -626,12 +623,7 @@ fn test_encryption_decryption() { } } -unsafe extern "C" fn cb( - _context: &Context, - _event: Event, - _data1: uintptr_t, - _data2: uintptr_t, -) -> uintptr_t { +fn cb(_context: &Context, _event: Event, _data1: uintptr_t, _data2: uintptr_t) -> uintptr_t { 0 } @@ -641,15 +633,10 @@ struct TestContext { dir: TempDir, } -unsafe fn create_test_context() -> TestContext { - let mut ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None); +fn create_test_context() -> TestContext { let dir = tempdir().unwrap(); let dbfile = dir.path().join("db.sqlite"); - assert!( - dc_open(&mut ctx, dbfile.to_str().unwrap(), None), - "Failed to open {}", - dbfile.display() - ); + let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap(); TestContext { ctx: ctx, dir: dir } } @@ -686,7 +673,7 @@ fn test_dc_mimeparser_with_context() { #[test] fn test_dc_get_oauth2_url() { - let ctx = unsafe { create_test_context() }; + let ctx = create_test_context(); let addr = "dignifiedquire@gmail.com"; let redirect_uri = "chat.delta:/com.b44t.messenger"; let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri); @@ -696,7 +683,7 @@ fn test_dc_get_oauth2_url() { #[test] fn test_dc_get_oauth2_addr() { - let ctx = unsafe { create_test_context() }; + let ctx = create_test_context(); let addr = "dignifiedquire@gmail.com"; let code = "fail"; let res = dc_get_oauth2_addr(&ctx.ctx, addr, code); @@ -706,7 +693,7 @@ fn test_dc_get_oauth2_addr() { #[test] fn test_dc_get_oauth2_token() { - let ctx = unsafe { create_test_context() }; + let ctx = create_test_context(); let addr = "dignifiedquire@gmail.com"; let code = "fail"; let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, 0); @@ -724,20 +711,18 @@ fn test_stress_tests() { #[test] fn test_get_contacts() { - unsafe { - let context = create_test_context(); - let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap(); - assert_eq!(contacts.len(), 0); + let context = create_test_context(); + let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap(); + assert_eq!(contacts.len(), 0); - let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap(); - assert_ne!(id, 0); + let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap(); + assert_ne!(id, 0); - let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap(); - assert_eq!(contacts.len(), 1); + let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap(); + assert_eq!(contacts.len(), 1); - let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap(); - assert_eq!(contacts.len(), 0); - } + let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap(); + assert_eq!(contacts.len(), 0); } #[test] @@ -758,16 +743,3 @@ fn test_chat() { assert_eq!(chat2.name, chat.name); } } - -#[test] -fn test_wrong_db() { - unsafe { - let mut ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None); - let dir = tempdir().unwrap(); - let dbfile = dir.path().join("db.sqlite"); - std::fs::write(&dbfile, b"123").unwrap(); - - let res = dc_open(&mut ctx, dbfile.to_str().unwrap(), None); - assert!(!res); - } -}