void dc_forward_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id)
{
	dc_msg_t*      msg = dc_msg_new_untyped(context);
	dc_chat_t*     chat = dc_chat_new(context);
	dc_contact_t*  contact = dc_contact_new(context);
	int            transaction_pending = 0;
	carray*        created_db_entries = carray_new(16);
	char*          idsstr = NULL;
	char*          q3 = NULL;
	sqlite3_stmt*  stmt = NULL;
	time_t         curr_timestamp = 0;
	dc_param_t*    original_param = dc_param_new();

	if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) {
		goto cleanup;
	}

	dc_sqlite3_begin_transaction(context->sql);
	transaction_pending = 1;

		dc_unarchive_chat(context, chat_id);

		context->smtp->log_connect_errors = 1;

		if (!dc_chat_load_from_db(chat, chat_id)) {
			goto cleanup;
		}

		curr_timestamp = dc_create_smeared_timestamps(context, msg_cnt);

		idsstr = dc_arr_to_string(msg_ids, msg_cnt);
		q3 = sqlite3_mprintf("SELECT id FROM msgs WHERE id IN(%s) ORDER BY timestamp,id", idsstr);
		stmt = dc_sqlite3_prepare(context->sql, q3);
		while (sqlite3_step(stmt)==SQLITE_ROW)
		{
			int src_msg_id = sqlite3_column_int(stmt, 0);
			if (!dc_msg_load_from_db(msg, context, src_msg_id)) {
				goto cleanup;
			}

			dc_param_set_packed(original_param, msg->param->packed);

			// do not mark own messages as being forwarded.
			// this allows sort of broadcasting
			// by just forwarding messages to other chats.
			if (msg->from_id!=DC_CONTACT_ID_SELF) {
				dc_param_set_int(msg->param, DC_PARAM_FORWARDED, 1);
			}

			dc_param_set(msg->param, DC_PARAM_GUARANTEE_E2EE, NULL);
			dc_param_set(msg->param, DC_PARAM_FORCE_PLAINTEXT, NULL);
			dc_param_set(msg->param, DC_PARAM_CMD, NULL);

			uint32_t new_msg_id;
			// PREPARING messages can't be forwarded immediately
			if (msg->state==DC_STATE_OUT_PREPARING) {
				new_msg_id = prepare_msg_raw(context, chat, msg, curr_timestamp++);

				// to update the original message, perform in-place surgery
				// on msg to avoid copying the entire structure, text, etc.
				dc_param_t* save_param = msg->param;
				msg->param = original_param;
				msg->id = src_msg_id;
				{
					// append new id to the original's param.
					char* old_fwd = dc_param_get(msg->param, DC_PARAM_PREP_FORWARDS, "");
					char* new_fwd = dc_mprintf("%s %d", old_fwd, new_msg_id);
					dc_param_set(msg->param, DC_PARAM_PREP_FORWARDS, new_fwd);
					dc_msg_save_param_to_disk(msg);
					free(new_fwd);
					free(old_fwd);
				}
				msg->param = save_param;
			}
			else {
				msg->state = DC_STATE_OUT_PENDING;
				new_msg_id = prepare_msg_raw(context, chat, msg, curr_timestamp++);
				dc_job_send_msg(context, new_msg_id);
			}

			carray_add(created_db_entries, (void*)(uintptr_t)chat_id, NULL);
			carray_add(created_db_entries, (void*)(uintptr_t)new_msg_id, NULL);
		}

	dc_sqlite3_commit(context->sql);
	transaction_pending = 0;

cleanup:
	if (transaction_pending) { dc_sqlite3_rollback(context->sql); }
	if (created_db_entries) {
		size_t i, icnt = carray_count(created_db_entries);
		for (i = 0; i < icnt; i += 2) {
			context->cb(context, DC_EVENT_MSGS_CHANGED, (uintptr_t)carray_get(created_db_entries, i), (uintptr_t)carray_get(created_db_entries, i+1));
		}
		carray_free(created_db_entries);
	}
	dc_contact_unref(contact);
	dc_msg_unref(msg);
	dc_chat_unref(chat);
	sqlite3_finalize(stmt);
	free(idsstr);
	sqlite3_free(q3);
	dc_param_unref(original_param);
}

pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Result<()> {
    ensure!(!msg_ids.is_empty(), "empty msgs_ids: nothing to forward");
    ensure!(!chat_id.is_special(), "can not forward to special chat");

    let mut created_chats: Vec<ChatId> = Vec::new();
    let mut created_msgs: Vec<MsgId> = Vec::new();
    let mut curr_timestamp: i64;

    chat_id
        .unarchive_if_not_muted(context, MessageState::Undefined)
        .await?;
    let mut chat = Chat::load_from_db(context, chat_id).await?;
    if let Some(reason) = chat.why_cant_send(context).await? {
        bail!("cannot send to {}: {}", chat_id, reason);
    }
    curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
    let ids = context
        .sql
        .query_map(
            &format!(
                "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
                sql::repeat_vars(msg_ids.len())
            ),
            rusqlite::params_from_iter(msg_ids),
            |row| row.get::<_, MsgId>(0),
            |ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
        )
        .await?;

    for id in ids {
        let src_msg_id: MsgId = id;
        let mut msg = Message::load_from_db(context, src_msg_id).await?;
        if msg.state == MessageState::OutDraft {
            bail!("cannot forward drafts.");
        }

        let original_param = msg.param.clone();

        // we tested a sort of broadcast
        // by not marking own forwarded messages as such,
        // however, this turned out to be to confusing and unclear.

        if msg.get_viewtype() != Viewtype::Sticker {
            msg.param
                .set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
        }

        msg.param.remove(Param::GuaranteeE2ee);
        msg.param.remove(Param::ForcePlaintext);
        msg.param.remove(Param::Cmd);
        msg.param.remove(Param::OverrideSenderDisplayname);
        msg.param.remove(Param::WebxdcDocument);
        msg.param.remove(Param::WebxdcDocumentTimestamp);
        msg.param.remove(Param::WebxdcSummary);
        msg.param.remove(Param::WebxdcSummaryTimestamp);
        msg.in_reply_to = None;

        // do not leak data as group names; a default subject is generated by mimefactory
        msg.subject = "".to_string();

        let new_msg_id: MsgId;
        if msg.state == MessageState::OutPreparing {
            new_msg_id = chat
                .prepare_msg_raw(context, &mut msg, None, curr_timestamp)
                .await?;
            curr_timestamp += 1;
            msg.param = original_param;
            msg.id = src_msg_id;

            if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
                let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
                msg.param.set(Param::PrepForwards, new_fwd);
            } else {
                msg.param
                    .set(Param::PrepForwards, new_msg_id.to_u32().to_string());
            }

            msg.update_param(context).await?;
        } else {
            msg.state = MessageState::OutPending;
            new_msg_id = chat
                .prepare_msg_raw(context, &mut msg, None, curr_timestamp)
                .await?;
            curr_timestamp += 1;
            if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
                context.scheduler.interrupt_smtp().await;
            }
        }
        created_chats.push(chat_id);
        created_msgs.push(new_msg_id);
    }
    for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) {
        context.emit_msgs_changed(*chat_id, *msg_id);
    }
    Ok(())
}

pub(crate) async fn update_param(&mut self, context: &Context) -> Result<()> {
        context
            .sql
            .execute(
                "UPDATE chats SET param=? WHERE id=?",
                (self.param.to_string(), self.id),
            )
            .await?;
        Ok(())
    }

pub fn get_viewtype(&self) -> Viewtype {
        self.viewtype
    }

pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
    let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
    let mimefactory = MimeFactory::from_msg(context, msg).await?;
    let attach_selfavatar = mimefactory.attach_selfavatar;
    let mut recipients = mimefactory.recipients();

    let from = context.get_primary_self_addr().await?;
    let lowercase_from = from.to_lowercase();

    // Send BCC to self if it is enabled.
    //
    // Previous versions of Delta Chat did not send BCC self
    // if DeleteServerAfter was set to immediately delete messages
    // from the server. This is not the case anymore
    // because BCC-self messages are also used to detect
    // that message was sent if SMTP server is slow to respond
    // and connection is frequently lost
    // before receiving status line.
    //
    // `from` must be the last addr, see `receive_imf_inner()` why.
    if context.get_config_bool(Config::BccSelf).await?
        && !recipients
            .iter()
            .any(|x| x.to_lowercase() == lowercase_from)
    {
        recipients.push(from);
    }

    // Webxdc integrations are messages, however, shipped with main app and must not be sent out
    if msg.param.get_int(Param::WebxdcIntegration).is_some() {
        recipients.clear();
    }

    if recipients.is_empty() {
        // may happen eg. for groups with only SELF and bcc_self disabled
        info!(
            context,
            "Message {} has no recipient, skipping smtp-send.", msg.id
        );
        msg.id.set_delivered(context).await?;
        msg.state = MessageState::OutDelivered;
        return Ok(Vec::new());
    }

    let rendered_msg = match mimefactory.render(context).await {
        Ok(res) => Ok(res),
        Err(err) => {
            message::set_msg_failed(context, msg, &err.to_string()).await?;
            Err(err)
        }
    }?;

    if needs_encryption && !rendered_msg.is_encrypted {
        /* unrecoverable */
        message::set_msg_failed(
            context,
            msg,
            "End-to-end-encryption unavailable unexpectedly.",
        )
        .await?;
        bail!(
            "e2e encryption unavailable {} - {:?}",
            msg.id,
            needs_encryption
        );
    }

    let now = smeared_time(context);

    if rendered_msg.is_gossiped {
        msg.chat_id.set_gossiped_timestamp(context, now).await?;
    }

    if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
        // Reject member list synchronisation from older messages. See also
        // `receive_imf::apply_group_changes()`.
        msg.chat_id
            .update_timestamp(
                context,
                Param::MemberListTimestamp,
                now.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
            )
            .await?;
    }

    if rendered_msg.last_added_location_id.is_some() {
        if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
            error!(context, "Failed to set kml sent_timestamp: {err:#}.");
        }
    }

    if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
        if let Err(err) = context.delete_sync_ids(sync_ids).await {
            error!(context, "Failed to delete sync ids: {err:#}.");
        }
    }

    if attach_selfavatar {
        if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
            error!(context, "Failed to set selfavatar timestamp: {err:#}.");
        }
    }

    if rendered_msg.is_encrypted && !needs_encryption {
        msg.param.set_int(Param::GuaranteeE2ee, 1);
        msg.update_param(context).await?;
    }

    msg.subject.clone_from(&rendered_msg.subject);
    msg.update_subject(context).await?;
    let chunk_size = context.get_max_smtp_rcpt_to().await?;
    let trans_fn = |t: &mut rusqlite::Transaction| {
        let mut row_ids = Vec::<i64>::new();
        for recipients_chunk in recipients.chunks(chunk_size) {
            let recipients_chunk = recipients_chunk.join(" ");
            let row_id = t.execute(
                "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \
                VALUES            (?1,         ?2,         ?3,   ?4)",
                (
                    &rendered_msg.rfc724_mid,
                    recipients_chunk,
                    &rendered_msg.message,
                    msg.id,
                ),
            )?;
            row_ids.push(row_id.try_into()?);
        }
        Ok(row_ids)
    };
    context.sql.transaction(trans_fn).await
}

async fn prepare_msg_raw(
        &mut self,
        context: &Context,
        msg: &mut Message,
        update_msg_id: Option<MsgId>,
        timestamp: i64,
    ) -> Result<MsgId> {
        let mut to_id = 0;
        let mut location_id = 0;

        let new_rfc724_mid = create_outgoing_rfc724_mid();

        if self.typ == Chattype::Single {
            if let Some(id) = context
                .sql
                .query_get_value(
                    "SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
                    (self.id,),
                )
                .await?
            {
                to_id = id;
            } else {
                error!(
                    context,
                    "Cannot send message, contact for {} not found.", self.id,
                );
                bail!("Cannot set message, contact for {} not found.", self.id);
            }
        } else if self.typ == Chattype::Group
            && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
        {
            msg.param.set_int(Param::AttachGroupImage, 1);
            self.param.remove(Param::Unpromoted);
            self.update_param(context).await?;
            // send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
            // when the group-creation message is actually sent though SMTP -
            // this makes sure, the other devices are aware of grpid that is used in the sync-message.
            context
                .sync_qr_code_tokens(Some(self.id))
                .await
                .log_err(context)
                .ok();
        }

        // reset encrypt error state eg. for forwarding
        msg.param.remove(Param::ErroneousE2ee);

        let is_bot = context.get_config_bool(Config::Bot).await?;
        msg.param
            .set_optional(Param::Bot, Some("1").filter(|_| is_bot));

        // Set "In-Reply-To:" to identify the message to which the composed message is a reply.
        // Set "References:" to identify the "thread" of the conversation.
        // Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
        let new_references;
        if self.is_self_talk() {
            // As self-talks are mainly used to transfer data between devices,
            // we do not set In-Reply-To/References in this case.
            new_references = String::new();
        } else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
            // We don't filter `OutPending` and `OutFailed` messages because the new message for
            // which `parent_query()` is done may assume that it will be received in a context
            // affected by those messages, e.g. they could add new members to a group and the
            // new message will contain them in "To:". Anyway recipients must be prepared to
            // orphaned references.
            self
                .id
                .get_parent_mime_headers(context, MessageState::OutPending)
                .await?
        {
            // "In-Reply-To:" is not changed if it is set manually.
            // This does not affect "References:" header, it will contain "default parent" (the
            // latest message in the thread) anyway.
            if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
                msg.in_reply_to = Some(parent_rfc724_mid.clone());
            }

            // Use parent `In-Reply-To` as a fallback
            // in case parent message has no `References` header
            // as specified in RFC 5322:
            // > If the parent message does not contain
            // > a "References:" field but does have an "In-Reply-To:" field
            // > containing a single message identifier, then the "References:" field
            // > will contain the contents of the parent's "In-Reply-To:" field
            // > followed by the contents of the parent's "Message-ID:" field (if
            // > any).
            let parent_references = if parent_references.is_empty() {
                parent_in_reply_to
            } else {
                parent_references
            };

            // The whole list of messages referenced may be huge.
            // Only take 2 recent references and add third from `In-Reply-To`.
            let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
            references_vec.reverse();

            if !parent_rfc724_mid.is_empty()
                && !references_vec.contains(&parent_rfc724_mid.as_str())
            {
                references_vec.push(&parent_rfc724_mid)
            }

            if references_vec.is_empty() {
                // As a fallback, use our Message-ID,
                // same as in the case of top-level message.
                new_references = new_rfc724_mid.clone();
            } else {
                new_references = references_vec.join(" ");
            }
        } else {
            // This is a top-level message.
            // Add our Message-ID as first references.
            // This allows us to identify replies to our message even if
            // email server such as Outlook changes `Message-ID:` header.
            // MUAs usually keep the first Message-ID in `References:` header unchanged.
            new_references = new_rfc724_mid.clone();
        }

        // add independent location to database
        if msg.param.exists(Param::SetLatitude) {
            if let Ok(row_id) = context
                .sql
                .insert(
                    "INSERT INTO locations \
                     (timestamp,from_id,chat_id, latitude,longitude,independent)\
                     VALUES (?,?,?, ?,?,1);",
                    (
                        timestamp,
                        ContactId::SELF,
                        self.id,
                        msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
                        msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
                    ),
                )
                .await
            {
                location_id = row_id;
            }
        }

        let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
            EphemeralTimer::Disabled
        } else {
            self.id.get_ephemeral_timer(context).await?
        };
        let ephemeral_timestamp = match ephemeral_timer {
            EphemeralTimer::Disabled => 0,
            EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
        };

        let new_mime_headers = if msg.has_html() {
            let html = if msg.param.exists(Param::Forwarded) {
                msg.get_id().get_html(context).await?
            } else {
                msg.param.get(Param::SendHtml).map(|s| s.to_string())
            };
            match html {
                Some(html) => Some(tokio::task::block_in_place(move || {
                    buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
                })?),
                None => None,
            }
        } else {
            None
        };

        msg.chat_id = self.id;
        msg.from_id = ContactId::SELF;
        msg.rfc724_mid = new_rfc724_mid;
        msg.timestamp_sort = timestamp;

        // add message to the database
        if let Some(update_msg_id) = update_msg_id {
            context
                .sql
                .execute(
                    "UPDATE msgs
                     SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
                         state=?, txt=?, subject=?, param=?,
                         hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
                         mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
                         ephemeral_timestamp=?
                     WHERE id=?;",
                    params_slice![
                        msg.rfc724_mid,
                        msg.chat_id,
                        msg.from_id,
                        to_id,
                        msg.timestamp_sort,
                        msg.viewtype,
                        msg.state,
                        msg.text,
                        &msg.subject,
                        msg.param.to_string(),
                        msg.hidden,
                        msg.in_reply_to.as_deref().unwrap_or_default(),
                        new_references,
                        new_mime_headers.is_some(),
                        new_mime_headers.unwrap_or_default(),
                        location_id as i32,
                        ephemeral_timer,
                        ephemeral_timestamp,
                        update_msg_id
                    ],
                )
                .await?;
            msg.id = update_msg_id;
        } else {
            let raw_id = context
                .sql
                .insert(
                    "INSERT INTO msgs (
                        rfc724_mid,
                        chat_id,
                        from_id,
                        to_id,
                        timestamp,
                        type,
                        state,
                        txt,
                        subject,
                        param,
                        hidden,
                        mime_in_reply_to,
                        mime_references,
                        mime_modified,
                        mime_headers,
                        mime_compressed,
                        location_id,
                        ephemeral_timer,
                        ephemeral_timestamp)
                        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
                    params_slice![
                        msg.rfc724_mid,
                        msg.chat_id,
                        msg.from_id,
                        to_id,
                        msg.timestamp_sort,
                        msg.viewtype,
                        msg.state,
                        msg.text,
                        &msg.subject,
                        msg.param.to_string(),
                        msg.hidden,
                        msg.in_reply_to.as_deref().unwrap_or_default(),
                        new_references,
                        new_mime_headers.is_some(),
                        new_mime_headers.unwrap_or_default(),
                        location_id as i32,
                        ephemeral_timer,
                        ephemeral_timestamp
                    ],
                )
                .await?;
            context.new_msgs_notify.notify_one();
            msg.id = MsgId::new(u32::try_from(raw_id)?);

            maybe_set_logging_xdc(context, msg, self.id).await?;
            context.update_webxdc_integration_database(msg).await?;
        }
        context.scheduler.interrupt_ephemeral_task().await;
        Ok(msg.id)
    }

pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
        self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
        chatlist_events::emit_chatlist_changed(self);
        chatlist_events::emit_chatlist_item_changed(self, chat_id);
    }

pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
        self.set(key, format!("{value}"));
        self
    }

pub async fn query_map<T, F, G, H>(
        &self,
        sql: &str,
        params: impl rusqlite::Params + Send,
        f: F,
        mut g: G,
    ) -> Result<H>
    where
        F: Send + FnMut(&rusqlite::Row) -> rusqlite::Result<T>,
        G: Send + FnMut(rusqlite::MappedRows<F>) -> Result<H>,
        H: Send + 'static,
    {
        self.call(move |conn| {
            let mut stmt = conn.prepare(sql)?;
            let res = stmt.query_map(params, f)?;
            g(res)
        })
        .await
    }

pub(crate) fn iter(&self) -> BlobDirIter<'_> {
        BlobDirIter::new(self.context, self.inner.iter())
    }

pub fn remove(&mut self, key: Param) -> &mut Self {
        self.inner.remove(&key);
        self
    }

pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

pub fn is_special(self) -> bool {
        (0..=DC_CHAT_ID_LAST_SPECIAL.0).contains(&self.0)
    }

pub fn to_u32(self) -> u32 {
        self.0
    }

pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
        use CantSendReason::*;

        // NB: Don't forget to update Chatlist::try_load() when changing this function!
        let reason = if self.id.is_special() {
            Some(SpecialChat)
        } else if self.is_device_talk() {
            Some(DeviceChat)
        } else if self.is_contact_request() {
            Some(ContactRequest)
        } else if self.is_protection_broken() {
            Some(ProtectionBroken)
        } else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
            Some(ReadOnlyMailingList)
        } else if !self.is_self_in_chat(context).await? {
            Some(NotAMember)
        } else if self
            .check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
            .await?
            > 0
        {
            Some(SecurejoinWait)
        } else {
            None
        };
        Ok(reason)
    }

pub async fn unarchive_if_not_muted(
        self,
        context: &Context,
        msg_state: MessageState,
    ) -> Result<()> {
        if msg_state != MessageState::InFresh {
            context
                .sql
                .execute(
                    "UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
                AND NOT(muted_until=-1 OR muted_until>?)",
                    (self, time()),
                )
                .await?;
            return Ok(());
        }
        let chat = Chat::load_from_db(context, self).await?;
        if chat.visibility != ChatVisibility::Archived {
            return Ok(());
        }
        if chat.is_muted() {
            let unread_cnt = context
                .sql
                .count(
                    "SELECT COUNT(*)
                FROM msgs
                WHERE state=?
                AND hidden=0
                AND chat_id=?",
                    (MessageState::InFresh, self),
                )
                .await?;
            if unread_cnt == 1 {
                // Added the first unread message in the chat.
                context.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
            }
            return Ok(());
        }
        context
            .sql
            .execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
            .await?;
        Ok(())
    }

pub async fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self> {
        let mut chat = context
            .sql
            .query_row(
                "SELECT c.type, c.name, c.grpid, c.param, c.archived,
                    c.blocked, c.locations_send_until, c.muted_until, c.protected
             FROM chats c
             WHERE c.id=?;",
                (chat_id,),
                |row| {
                    let c = Chat {
                        id: chat_id,
                        typ: row.get(0)?,
                        name: row.get::<_, String>(1)?,
                        grpid: row.get::<_, String>(2)?,
                        param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
                        visibility: row.get(4)?,
                        blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
                        is_sending_locations: row.get(6)?,
                        mute_duration: row.get(7)?,
                        protected: row.get(8)?,
                    };
                    Ok(c)
                },
            )
            .await
            .context(format!("Failed loading chat {chat_id} from database"))?;

        if chat.id.is_archived_link() {
            chat.name = stock_str::archived_chats(context).await;
        } else {
            if chat.typ == Chattype::Single && chat.name.is_empty() {
                // chat.name is set to contact.display_name on changes,
                // however, if things went wrong somehow, we do this here explicitly.
                let mut chat_name = "Err [Name not found]".to_owned();
                match get_chat_contacts(context, chat.id).await {
                    Ok(contacts) => {
                        if let Some(contact_id) = contacts.first() {
                            if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
                                contact.get_display_name().clone_into(&mut chat_name);
                            }
                        }
                    }
                    Err(err) => {
                        error!(
                            context,
                            "Failed to load contacts for {}: {:#}.", chat.id, err
                        );
                    }
                }
                chat.name = chat_name;
            }
            if chat.param.exists(Param::Selftalk) {
                chat.name = stock_str::saved_messages(context).await;
            } else if chat.param.exists(Param::Devicetalk) {
                chat.name = stock_str::device_messages(context).await;
            }
        }

        Ok(chat)
    }

pub async fn query_map<T, F, G, H>(
        &self,
        sql: &str,
        params: impl rusqlite::Params + Send,
        f: F,
        mut g: G,
    ) -> Result<H>
    where
        F: Send + FnMut(&rusqlite::Row) -> rusqlite::Result<T>,
        G: Send + FnMut(rusqlite::MappedRows<F>) -> Result<H>,
        H: Send + 'static,
    {
        self.call(move |conn| {
            let mut stmt = conn.prepare(sql)?;
            let res = stmt.query_map(params, f)?;
            g(res)
        })
        .await
    }

pub fn params_from_iter<I>(iter: I) -> ParamsFromIter<I>
where
    I: IntoIterator,
    I::Item: ToSql,
{
    ParamsFromIter(iter)
}

pub(crate) fn create_smeared_timestamps(context: &Context, count: usize) -> i64 {
    let now = time();
    context.smeared_timestamp.create_n(now, count as i64)
}

pub fn repeat_vars(count: usize) -> String {
    let mut s = "?,".repeat(count);
    s.pop(); // Remove trailing comma
    s
}

pub fn set(&mut self, key: Param, value: impl ToString) -> &mut Self {
        self.inner.insert(key, value.to_string());
        self
    }

pub fn len(&self) -> usize {
        self.inner.len()
    }

pub struct Context {
    pub(crate) inner: Arc<InnerContext>,
}

pub struct MsgId(u32);

pub struct ChatId(u32);

pub enum Param {
    /// For messages
    File = b'f',

    /// For messages: original filename (as shown in chat)
    Filename = b'v',

    /// For messages: This name should be shown instead of contact.get_display_name()
    /// (used if this is a mailinglist
    /// or explicitly set using set_override_sender_name(), eg. by bots)
    OverrideSenderDisplayname = b'O',

    /// For Messages
    Width = b'w',

    /// For Messages
    Height = b'h',

    /// For Messages
    Duration = b'd',

    /// For Messages
    MimeType = b'm',

    /// For Messages: HTML to be written to the database and to be send.
    /// `SendHtml` param is not used for received messages.
    /// Use `MsgId::get_html()` to get HTML of received messages.
    SendHtml = b'T',

    /// For Messages: message is encrypted, outgoing: guarantee E2EE or the message is not send
    GuaranteeE2ee = b'c',

    /// For Messages: quoted message is encrypted.
    ///
    /// If this message is sent unencrypted, quote text should be replaced.
    ProtectQuote = b'0',

    /// For Messages: decrypted with validation errors or without mutual set, if neither
    /// 'c' nor 'e' are preset, the messages is only transport encrypted.
    ErroneousE2ee = b'e',

    /// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
    ForcePlaintext = b'u',

    /// For Messages: do not include Autocrypt header.
    SkipAutocrypt = b'o',

    /// For Messages
    WantsMdn = b'r',

    /// For Messages: the message is a reaction.
    Reaction = b'x',

    /// For Chats: the timestamp of the last reaction.
    LastReactionTimestamp = b'y',

    /// For Chats: Message ID of the last reaction.
    LastReactionMsgId = b'Y',

    /// For Chats: Contact ID of the last reaction.
    LastReactionContactId = b'1',

    /// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
    Bot = b'b',

    /// For Messages: unset or 0=not forwarded,
    /// 1=forwarded from unknown msg_id, >9 forwarded from msg_id
    Forwarded = b'a',

    /// For Messages: quoted text.
    Quote = b'q',

    /// For Messages
    Cmd = b'S',

    /// For Messages
    Arg = b'E',

    /// For Messages
    Arg2 = b'F',

    /// `Secure-Join-Fingerprint` header for `{vc,vg}-request-with-auth` messages.
    Arg3 = b'G',

    /// Deprecated `Secure-Join-Group` header for messages.
    Arg4 = b'H',

    /// For Messages
    AttachGroupImage = b'A',

    /// For Messages
    WebrtcRoom = b'V',

    /// For Messages: space-separated list of messaged IDs of forwarded copies.
    ///
    /// This is used when a [crate::message::Message] is in the
    /// [crate::message::MessageState::OutPending] state but is already forwarded.
    /// In this case the forwarded messages are written to the
    /// database and their message IDs are added to this parameter of
    /// the original message, which is also saved in the database.
    /// When the original message is then finally sent this parameter
    /// is used to also send all the forwarded messages.
    PrepForwards = b'P',

    /// For Messages
    SetLatitude = b'l',

    /// For Messages
    SetLongitude = b'n',

    /// For Groups
    ///
    /// An unpromoted group has not had any messages sent to it and thus only exists on the
    /// creator's device.  Any changes made to an unpromoted group do not need to send
    /// system messages to the group members to update them of the changes.  Once a message
    /// has been sent to a group it is promoted and group changes require sending system
    /// messages to all members.
    Unpromoted = b'U',

    /// For Groups and Contacts
    ProfileImage = b'i',

    /// For Chats
    /// Signals whether the chat is the `saved messages` chat
    Selftalk = b'K',

    /// For Chats: On sending a new message we set the subject to `Re: <last subject>`.
    /// Usually we just use the subject of the parent message, but if the parent message
    /// is deleted, we use the LastSubject of the chat.
    LastSubject = b't',

    /// For Chats
    Devicetalk = b'D',

    /// For Chats: If this is a mailing list chat, contains the List-Post address.
    /// None if there simply is no `List-Post` header in the mailing list.
    /// Some("") if the mailing list is using multiple different List-Post headers.
    ///
    /// The List-Post address is the email address where the user can write to in order to
    /// post something to the mailing list.
    ListPost = b'p',

    /// For Contacts: If this is the List-Post address of a mailing list, contains
    /// the List-Id of the mailing list (which is also used as the group id of the chat).
    ListId = b's',

    /// For Contacts: timestamp of status (aka signature or footer) update.
    StatusTimestamp = b'j',

    /// For Contacts and Chats: timestamp of avatar update.
    AvatarTimestamp = b'J',

    /// For Chats: timestamp of status/signature/footer update.
    EphemeralSettingsTimestamp = b'B',

    /// For Chats: timestamp of subject update.
    SubjectTimestamp = b'C',

    /// For Chats: timestamp of group name update.
    GroupNameTimestamp = b'g',

    /// For Chats: timestamp of member list update.
    MemberListTimestamp = b'k',

    /// For Webxdc Message Instances: Current document name
    WebxdcDocument = b'R',

    /// For Webxdc Message Instances: timestamp of document name update.
    WebxdcDocumentTimestamp = b'W',

    /// For Webxdc Message Instances: Current summary
    WebxdcSummary = b'N',

    /// For Webxdc Message Instances: timestamp of summary update.
    WebxdcSummaryTimestamp = b'Q',

    /// For Webxdc Message Instances: Webxdc is an integration, see init_webxdc_integration()
    WebxdcIntegration = b'3',

    /// For Webxdc Message Instances: Chat to integrate the Webxdc for.
    WebxdcIntegrateFor = b'2',

    /// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
    ForceSticker = b'X',
    // 'L' was defined as ProtectionSettingsTimestamp for Chats, however, never used in production.
}

pub enum Viewtype {
    /// Unknown message type.
    #[default]
    Unknown = 0,

    /// Text message.
    /// The text of the message is set using dc_msg_set_text() and retrieved with dc_msg_get_text().
    Text = 10,

    /// Image message.
    /// If the image is a GIF and has the appropriate extension, the viewtype is auto-changed to
    /// `Gif` when sending the message.
    /// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
    /// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
    Image = 20,

    /// Animated GIF message.
    /// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
    /// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
    Gif = 21,

    /// Message containing a sticker, similar to image.
    /// If possible, the ui should display the image without borders in a transparent way.
    /// A click on a sticker will offer to install the sticker set in some future.
    Sticker = 23,

    /// Message containing an Audio file.
    /// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
    /// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
    Audio = 40,

    /// A voice message that was directly recorded by the user.
    /// For all other audio messages, the type #DC_MSG_AUDIO should be used.
    /// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
    /// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
    Voice = 41,

    /// Video messages.
    /// File, width, height and durarion
    /// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
    /// and retrieved via
    /// dc_msg_get_file(), dc_msg_get_width(),
    /// dc_msg_get_height(), dc_msg_get_duration().
    Video = 50,

    /// Message containing any file, eg. a PDF.
    /// The file is set via dc_msg_set_file()
    /// and retrieved via dc_msg_get_file().
    File = 60,

    /// Message is an invitation to a videochat.
    VideochatInvitation = 70,

    /// Message is an webxdc instance.
    Webxdc = 80,

    /// Message containing shared contacts represented as a vCard (virtual contact file)
    /// with email addresses and possibly other fields.
    /// Use `parse_vcard()` to retrieve them.
    Vcard = 90,
}

pub struct InnerContext {
    /// Blob directory path
    pub(crate) blobdir: PathBuf,
    pub(crate) sql: Sql,
    pub(crate) smeared_timestamp: SmearedTimestamp,
    /// The global "ongoing" process state.
    ///
    /// This is a global mutex-like state for operations which should be modal in the
    /// clients.
    running_state: RwLock<RunningState>,
    /// Mutex to avoid generating the key for the user more than once.
    pub(crate) generating_key_mutex: Mutex<()>,
    /// Mutex to enforce only a single running oauth2 is running.
    pub(crate) oauth2_mutex: Mutex<()>,
    /// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent.
    pub(crate) wrong_pw_warning_mutex: Mutex<()>,
    pub(crate) translated_stockstrings: StockStrings,
    pub(crate) events: Events,

    pub(crate) scheduler: SchedulerState,
    pub(crate) ratelimit: RwLock<Ratelimit>,

    /// Recently loaded quota information, if any.
    /// Set to `None` if quota was never tried to load.
    pub(crate) quota: RwLock<Option<QuotaInfo>>,

    /// IMAP UID resync request.
    pub(crate) resync_request: AtomicBool,

    /// Notify about new messages.
    ///
    /// This causes [`Context::wait_next_msgs`] to wake up.
    pub(crate) new_msgs_notify: Notify,

    /// Server ID response if ID capability is supported
    /// and the server returned non-NIL on the inbox connection.
    /// <https://datatracker.ietf.org/doc/html/rfc2971>
    pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,

    /// IMAP METADATA.
    pub(crate) metadata: RwLock<Option<ServerMetadata>>,

    pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,

    /// ID for this `Context` in the current process.
    ///
    /// This allows for multiple `Context`s open in a single process where each context can
    /// be identified by this ID.
    pub(crate) id: u32,

    creation_time: tools::Time,

    /// The text of the last error logged and emitted as an event.
    /// If the ui wants to display an error after a failure,
    /// `last_error` should be used to avoid races with the event thread.
    pub(crate) last_error: std::sync::RwLock<String>,

    /// If debug logging is enabled, this contains all necessary information
    ///
    /// Standard RwLock instead of [`tokio::sync::RwLock`] is used
    /// because the lock is used from synchronous [`Context::emit_event`].
    pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,

    /// Push subscriber to store device token
    /// and register for heartbeat notifications.
    pub(crate) push_subscriber: PushSubscriber,

    /// True if account has subscribed to push notifications via IMAP.
    pub(crate) push_subscribed: AtomicBool,

    /// Iroh for realtime peer channels.
    pub(crate) iroh: OnceCell<Iroh>,
}

pub enum MessageState {
    /// Undefined message state.
    #[default]
    Undefined = 0,

    /// Incoming *fresh* message. Fresh messages are neither noticed
    /// nor seen and are typically shown in notifications.
    InFresh = 10,

    /// Incoming *noticed* message. E.g. chat opened but message not
    /// yet read - noticed messages are not counted as unread but did
    /// not marked as read nor resulted in MDNs.
    InNoticed = 13,

    /// Incoming message, really *seen* by the user. Marked as read on
    /// IMAP and MDN may be sent.
    InSeen = 16,

    /// For files which need time to be prepared before they can be
    /// sent, the message enters this state before
    /// OutPending.
    OutPreparing = 18,

    /// Message saved as draft.
    OutDraft = 19,

    /// The user has pressed the "send" button but the message is not
    /// yet sent and is pending in some way. Maybe we're offline (no
    /// checkmark).
    OutPending = 20,

    /// *Unrecoverable* error (*recoverable* errors result in pending
    /// messages).
    OutFailed = 24,

    /// Outgoing message successfully delivered to server (one
    /// checkmark). Note, that already delivered messages may get into
    /// the OutFailed state if we get such a hint from the server.
    OutDelivered = 26,

    /// Outgoing message read by the recipient (two checkmarks; this
    /// requires goodwill on the receiver's side)
    OutMdnRcvd = 28,
}


int dc_delete_contact(dc_context_t* context, uint32_t contact_id)
{
	int           success = 0;
	sqlite3_stmt* stmt = NULL;

	if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) {
		goto cleanup;
	}

	/* we can only delete contacts that are not in use anywhere; this function is mainly for the user who has just
	created an contact manually and wants to delete it a moment later */
	stmt = dc_sqlite3_prepare(context->sql,
		"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;");
	sqlite3_bind_int(stmt, 1, contact_id);
	if (sqlite3_step(stmt)!=SQLITE_ROW || sqlite3_column_int(stmt, 0) >= 1) {
		goto cleanup;
	}
	sqlite3_finalize(stmt);
	stmt = NULL;

	stmt = dc_sqlite3_prepare(context->sql,
		"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;");
	sqlite3_bind_int(stmt, 1, contact_id);
	sqlite3_bind_int(stmt, 2, contact_id);
	if (sqlite3_step(stmt)!=SQLITE_ROW || sqlite3_column_int(stmt, 0) >= 1) {
		goto cleanup;
	}
	sqlite3_finalize(stmt);
	stmt = NULL;

	stmt = dc_sqlite3_prepare(context->sql,
		"DELETE FROM contacts WHERE id=?;");
	sqlite3_bind_int(stmt, 1, contact_id);
	if (sqlite3_step(stmt)!=SQLITE_DONE) {
		goto cleanup;
	}

	context->cb(context, DC_EVENT_CONTACTS_CHANGED, 0, 0);

	success = 1;

cleanup:
	sqlite3_finalize(stmt);
	return success;
}

pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
        ensure!(!contact_id.is_special(), "Can not delete special contact");

        context
            .sql
            .transaction(move |transaction| {
                // make sure, the transaction starts with a write command and becomes EXCLUSIVE by that -
                // upgrading later may be impossible by races.
                let deleted_contacts = transaction.execute(
                    "DELETE FROM contacts WHERE id=?
                     AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
                    (contact_id, contact_id),
                )?;
                if deleted_contacts == 0 {
                    transaction.execute(
                        "UPDATE contacts SET origin=? WHERE id=?;",
                        (Origin::Hidden, contact_id),
                    )?;
                }
                Ok(())
            })
            .await?;

        context.emit_event(EventType::ContactsChanged(None));
        Ok(())
    }

pub fn emit_event(&self, event: EventType) {
        {
            let lock = self.debug_logging.read().expect("RwLock is poisoned");
            if let Some(debug_logging) = &*lock {
                debug_logging.log_event(event.clone());
            }
        }
        self.events.emit(Event {
            id: self.id,
            typ: event,
        });
    }

pub fn is_special(&self) -> bool {
        self.0 <= Self::LAST_SPECIAL.0
    }

pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
    where
        H: Send + 'static,
        G: Send + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
    {
        self.call_write(move |conn| {
            let mut transaction = conn.transaction()?;
            let ret = callback(&mut transaction);

            match ret {
                Ok(ret) => {
                    transaction.commit()?;
                    Ok(ret)
                }
                Err(err) => {
                    transaction.rollback()?;
                    Err(err)
                }
            }
        })
        .await
    }

pub struct ContactId(u32);

pub struct Context {
    pub(crate) inner: Arc<InnerContext>,
}

pub struct InnerContext {
    /// Blob directory path
    pub(crate) blobdir: PathBuf,
    pub(crate) sql: Sql,
    pub(crate) smeared_timestamp: SmearedTimestamp,
    /// The global "ongoing" process state.
    ///
    /// This is a global mutex-like state for operations which should be modal in the
    /// clients.
    running_state: RwLock<RunningState>,
    /// Mutex to avoid generating the key for the user more than once.
    pub(crate) generating_key_mutex: Mutex<()>,
    /// Mutex to enforce only a single running oauth2 is running.
    pub(crate) oauth2_mutex: Mutex<()>,
    /// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent.
    pub(crate) wrong_pw_warning_mutex: Mutex<()>,
    pub(crate) translated_stockstrings: StockStrings,
    pub(crate) events: Events,

    pub(crate) scheduler: SchedulerState,
    pub(crate) ratelimit: RwLock<Ratelimit>,

    /// Recently loaded quota information, if any.
    /// Set to `None` if quota was never tried to load.
    pub(crate) quota: RwLock<Option<QuotaInfo>>,

    /// IMAP UID resync request.
    pub(crate) resync_request: AtomicBool,

    /// Notify about new messages.
    ///
    /// This causes [`Context::wait_next_msgs`] to wake up.
    pub(crate) new_msgs_notify: Notify,

    /// Server ID response if ID capability is supported
    /// and the server returned non-NIL on the inbox connection.
    /// <https://datatracker.ietf.org/doc/html/rfc2971>
    pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,

    /// IMAP METADATA.
    pub(crate) metadata: RwLock<Option<ServerMetadata>>,

    pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,

    /// ID for this `Context` in the current process.
    ///
    /// This allows for multiple `Context`s open in a single process where each context can
    /// be identified by this ID.
    pub(crate) id: u32,

    creation_time: tools::Time,

    /// The text of the last error logged and emitted as an event.
    /// If the ui wants to display an error after a failure,
    /// `last_error` should be used to avoid races with the event thread.
    pub(crate) last_error: std::sync::RwLock<String>,

    /// If debug logging is enabled, this contains all necessary information
    ///
    /// Standard RwLock instead of [`tokio::sync::RwLock`] is used
    /// because the lock is used from synchronous [`Context::emit_event`].
    pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,

    /// Push subscriber to store device token
    /// and register for heartbeat notifications.
    pub(crate) push_subscriber: PushSubscriber,

    /// True if account has subscribed to push notifications via IMAP.
    pub(crate) push_subscribed: AtomicBool,

    /// Iroh for realtime peer channels.
    pub(crate) iroh: OnceCell<Iroh>,
}

pub enum Origin {
    /// Unknown origin. Can be used as a minimum origin to specify that the caller does not care
    /// about origin of the contact.
    #[default]
    Unknown = 0,

    /// The contact is a mailing list address, needed to unblock mailing lists
    MailinglistAddress = 0x2,

    /// Hidden on purpose, e.g. addresses with the word "noreply" in it
    Hidden = 0x8,

    /// From: of incoming messages of unknown sender
    IncomingUnknownFrom = 0x10,

    /// Cc: of incoming messages of unknown sender
    IncomingUnknownCc = 0x20,

    /// To: of incoming messages of unknown sender
    IncomingUnknownTo = 0x40,

    /// address scanned but not verified
    UnhandledQrScan = 0x80,

    /// Reply-To: of incoming message of known sender
    /// Contacts with at least this origin value are shown in the contact list.
    IncomingReplyTo = 0x100,

    /// Cc: of incoming message of known sender
    IncomingCc = 0x200,

    /// additional To:'s of incoming message of known sender
    IncomingTo = 0x400,

    /// a chat was manually created for this user, but no message yet sent
    CreateChat = 0x800,

    /// message sent by us
    OutgoingBcc = 0x1000,

    /// message sent by us
    OutgoingCc = 0x2000,

    /// message sent by us
    OutgoingTo = 0x4000,

    /// internal use
    Internal = 0x40000,

    /// address is in our address book
    AddressBook = 0x80000,

    /// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
    SecurejoinInvited = 0x0100_0000,

    /// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
    SecurejoinJoined = 0x0200_0000,

    /// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
    ManuallyCreated = 0x0400_0000,
}

pub enum EventType {
    /// The library-user may write an informational string to the log.
    ///
    /// This event should *not* be reported to the end-user using a popup or something like
    /// that.
    Info(String),

    /// Emitted when SMTP connection is established and login was successful.
    SmtpConnected(String),

    /// Emitted when IMAP connection is established and login was successful.
    ImapConnected(String),

    /// Emitted when a message was successfully sent to the SMTP server.
    SmtpMessageSent(String),

    /// Emitted when an IMAP message has been marked as deleted
    ImapMessageDeleted(String),

    /// Emitted when an IMAP message has been moved
    ImapMessageMoved(String),

    /// Emitted before going into IDLE on the Inbox folder.
    ImapInboxIdle,

    /// Emitted when an new file in the $BLOBDIR was created
    NewBlobFile(String),

    /// Emitted when an file in the $BLOBDIR was deleted
    DeletedBlobFile(String),

    /// The library-user should write a warning string to the log.
    ///
    /// This event should *not* be reported to the end-user using a popup or something like
    /// that.
    Warning(String),

    /// The library-user should report an error to the end-user.
    ///
    /// As most things are asynchronous, things may go wrong at any time and the user
    /// should not be disturbed by a dialog or so.  Instead, use a bubble or so.
    ///
    /// However, for ongoing processes (eg. configure())
    /// or for functions that are expected to fail (eg. dc_continue_key_transfer())
    /// it might be better to delay showing these events until the function has really
    /// failed (returned false). It should be sufficient to report only the *last* error
    /// in a messasge box then.
    Error(String),

    /// An action cannot be performed because the user is not in the group.
    /// Reported eg. after a call to
    /// dc_set_chat_name(), dc_set_chat_profile_image(),
    /// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
    /// dc_send_text_msg() or another sending function.
    ErrorSelfNotInGroup(String),

    /// Messages or chats changed.  One or more messages or chats changed for various
    /// reasons in the database:
    /// - Messages sent, received or removed
    /// - Chats created, deleted or archived
    /// - A draft has been set
    ///
    MsgsChanged {
        /// Set if only a single chat is affected by the changes, otherwise 0.
        chat_id: ChatId,

        /// Set if only a single message is affected by the changes, otherwise 0.
        msg_id: MsgId,
    },

    /// Reactions for the message changed.
    ReactionsChanged {
        /// ID of the chat which the message belongs to.
        chat_id: ChatId,

        /// ID of the message for which reactions were changed.
        msg_id: MsgId,

        /// ID of the contact whose reaction set is changed.
        contact_id: ContactId,
    },

    /// There is a fresh message. Typically, the user will show an notification
    /// when receiving this message.
    ///
    /// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
    IncomingMsg {
        /// ID of the chat where the message is assigned.
        chat_id: ChatId,

        /// ID of the message.
        msg_id: MsgId,
    },

    /// Downloading a bunch of messages just finished.
    IncomingMsgBunch,

    /// Messages were seen or noticed.
    /// chat id is always set.
    MsgsNoticed(ChatId),

    /// A single message is sent successfully. State changed from  DC_STATE_OUT_PENDING to
    /// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
    MsgDelivered {
        /// ID of the chat which the message belongs to.
        chat_id: ChatId,

        /// ID of the message that was successfully sent.
        msg_id: MsgId,
    },

    /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
    /// DC_STATE_OUT_FAILED, see dc_msg_get_state().
    MsgFailed {
        /// ID of the chat which the message belongs to.
        chat_id: ChatId,

        /// ID of the message that could not be sent.
        msg_id: MsgId,
    },

    /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
    /// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
    MsgRead {
        /// ID of the chat which the message belongs to.
        chat_id: ChatId,

        /// ID of the message that was read.
        msg_id: MsgId,
    },

    /// A single message was deleted.
    ///
    /// This event means that the message will no longer appear in the messagelist.
    /// UI should remove the message from the messagelist
    /// in response to this event if the message is currently displayed.
    ///
    /// The message may have been explicitly deleted by the user or expired.
    /// Internally the message may have been removed from the database,
    /// moved to the trash chat or hidden.
    ///
    /// This event does not indicate the message
    /// deletion from the server.
    MsgDeleted {
        /// ID of the chat where the message was prior to deletion.
        /// Never 0 or trash chat.
        chat_id: ChatId,

        /// ID of the deleted message. Never 0.
        msg_id: MsgId,
    },

    /// Chat changed.  The name or the image of a chat group was changed or members were added or removed.
    /// Or the verify state of a chat has changed.
    /// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
    /// and dc_remove_contact_from_chat().
    ///
    /// This event does not include ephemeral timer modification, which
    /// is a separate event.
    ChatModified(ChatId),

    /// Chat ephemeral timer changed.
    ChatEphemeralTimerModified {
        /// Chat ID.
        chat_id: ChatId,

        /// New ephemeral timer value.
        timer: EphemeralTimer,
    },

    /// Contact(s) created, renamed, blocked, deleted or changed their "recently seen" status.
    ///
    /// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
    ContactsChanged(Option<ContactId>),

    /// Location of one or more contact has changed.
    ///
    /// @param data1 (u32) contact_id of the contact for which the location has changed.
    ///     If the locations of several contacts have been changed,
    ///     eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
    LocationChanged(Option<ContactId>),

    /// Inform about the configuration progress started by configure().
    ConfigureProgress {
        /// Progress.
        ///
        /// 0=error, 1-999=progress in permille, 1000=success and done
        progress: usize,

        /// Progress comment or error, something to display to the user.
        comment: Option<String>,
    },

    /// Inform about the import/export progress started by imex().
    ///
    /// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
    /// @param data2 0
    ImexProgress(usize),

    /// A file has been exported. A file has been written by imex().
    /// This event may be sent multiple times by a single call to imex().
    ///
    /// A typical purpose for a handler of this event may be to make the file public to some system
    /// services.
    ///
    /// @param data2 0
    ImexFileWritten(PathBuf),

    /// Progress information of a secure-join handshake from the view of the inviter
    /// (Alice, the person who shows the QR code).
    ///
    /// These events are typically sent after a joiner has scanned the QR code
    /// generated by dc_get_securejoin_qr().
    SecurejoinInviterProgress {
        /// ID of the contact that wants to join.
        contact_id: ContactId,

        /// Progress as:
        /// 300=vg-/vc-request received, typically shown as "bob@addr joins".
        /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
        /// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
        /// 1000=Protocol finished for this contact.
        progress: usize,
    },

    /// Progress information of a secure-join handshake from the view of the joiner
    /// (Bob, the person who scans the QR code).
    /// The events are typically sent while dc_join_securejoin(), which
    /// may take some time, is executed.
    SecurejoinJoinerProgress {
        /// ID of the inviting contact.
        contact_id: ContactId,

        /// Progress as:
        /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
        /// (Bob has verified alice and waits until Alice does the same for him)
        /// 1000=vg-member-added/vc-contact-confirm received
        progress: usize,
    },

    /// The connectivity to the server changed.
    /// This means that you should refresh the connectivity view
    /// and possibly the connectivtiy HTML; see dc_get_connectivity() and
    /// dc_get_connectivity_html() for details.
    ConnectivityChanged,

    /// The user's avatar changed.
    /// Deprecated by `ConfigSynced`.
    SelfavatarChanged,

    /// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
    /// uniformity this is emitted on the source device too. The value isn't here, otherwise it
    /// would be logged which might not be good for privacy.
    ConfigSynced {
        /// Configuration key.
        key: Config,
    },

    /// Webxdc status update received.
    WebxdcStatusUpdate {
        /// Message ID.
        msg_id: MsgId,

        /// Status update ID.
        status_update_serial: StatusUpdateSerial,
    },

    /// Data received over an ephemeral peer channel.
    WebxdcRealtimeData {
        /// Message ID.
        msg_id: MsgId,

        /// Realtime data.
        data: Vec<u8>,
    },

    /// Inform that a message containing a webxdc instance has been deleted.
    WebxdcInstanceDeleted {
        /// ID of the deleted message.
        msg_id: MsgId,
    },

    /// Tells that the Background fetch was completed (or timed out).
    /// This event acts as a marker, when you reach this event you can be sure
    /// that all events emitted during the background fetch were processed.
    ///
    /// This event is only emitted by the account manager
    AccountsBackgroundFetchDone,
    /// Inform that set of chats or the order of the chats in the chatlist has changed.
    ///
    /// Sometimes this is emitted together with `UIChatlistItemChanged`.
    ChatlistChanged,

    /// Inform that a single chat list item changed and needs to be rerendered.
    /// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
    ChatlistItemChanged {
        /// ID of the changed chat
        chat_id: Option<ChatId>,
    },

    /// Event for using in tests, e.g. as a fence between normally generated events.
    #[cfg(test)]
    Test,

    /// Inform than some events have been skipped due to event channel overflow.
    EventChannelOverflow {
        /// Number of events skipped.
        n: u64,
    },
}

pub struct Contact {
    /// The contact ID.
    pub id: ContactId,

    /// Contact name. It is recommended to use `Contact::get_name`,
    /// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
    /// May be empty, initially set to `authname`.
    name: String,

    /// Name authorized by the contact himself. Only this name may be spread to others,
    /// e.g. in To:-lists. May be empty. It is recommended to use `Contact::get_authname`,
    /// to access this field.
    authname: String,

    /// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
    addr: String,

    /// Blocked state. Use contact_is_blocked to access this field.
    pub blocked: bool,

    /// Time when the contact was seen last time, Unix time in seconds.
    last_seen: i64,

    /// The origin/source of the contact.
    pub origin: Origin,

    /// Parameters as Param::ProfileImage
    pub param: Params,

    /// Last seen message signature for this contact, to be displayed in the profile.
    status: String,

    /// If the contact is a bot.
    is_bot: bool,
}

use std::cmp::{min, Reverse};
use std::collections::BinaryHeap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use base64::Engine as _;
use deltachat_contact_tools::may_be_valid_addr;
use deltachat_contact_tools::{
    self as contact_tools, addr_cmp, addr_normalize, sanitize_name_and_addr, strip_rtlo_characters,
    ContactAddress, VcardContact,
};
use deltachat_derive::{FromSql, ToSql};
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{timeout, Duration};
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
use crate::log::LogExt;
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*};
use crate::tools::{
    duration_to_str, get_abs_path, improve_single_line_input, smeared_time, time, SystemTime,
};
use crate::{chat, chatlist_events, stock_str};
use deltachat_contact_tools::{may_be_valid_addr, normalize_name};
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager};


* After the creation with dc_create_group_chat() the chat is usually unpromoted
 * until the first call to dc_send_text_msg() or another sending function.
 *
 * With unpromoted chats, members can be added
 * and settings can be modified without the need of special status messages being sent.
 *
 * While the core takes care of the unpromoted state on its own,
 * checking the state from the UI side may be useful to decide whether a hint as
 * "Send the first message to allow others to reply within the group"
 * should be shown to the user or not.
 *
 * @memberof dc_chat_t
 * @param chat The chat object.
 * @return 1=chat is still unpromoted, no message was ever send to the chat,
 *     0=chat is not unpromoted, messages were send and/or received
 *     or the chat is not group chat.
 */
int dc_chat_is_unpromoted(const dc_chat_t* chat)
{
	if (chat==NULL || chat->magic!=DC_CHAT_MAGIC) {
		return 0;
	}
	return dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0);
}

pub(crate) async fn is_unpromoted(self, context: &Context) -> Result<bool> {
        let param = self.get_param(context).await?;
        let unpromoted = param.get_bool(Param::Unpromoted).unwrap_or_default();
        Ok(unpromoted)
    }

pub fn get_bool(&self, key: Param) -> Option<bool> {
        self.get_int(key).map(|v| v != 0)
    }

pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
        let res: Option<String> = context
            .sql
            .query_get_value("SELECT param FROM chats WHERE id=?", (self,))
            .await?;
        Ok(res
            .map(|s| s.parse().unwrap_or_default())
            .unwrap_or_default())
    }

pub struct Context {
    pub(crate) inner: Arc<InnerContext>,
}

pub struct Chat {
    /// Database ID.
    pub id: ChatId,

    /// Chat type, e.g. 1:1 chat, group chat, mailing list.
    pub typ: Chattype,

    /// Chat name.
    pub name: String,

    /// Whether the chat is archived or pinned.
    pub visibility: ChatVisibility,

    /// Group ID. For [`Chattype::Mailinglist`] -- mailing list address. Empty for 1:1 chats and
    /// ad-hoc groups.
    pub grpid: String,

    /// Whether the chat is blocked, unblocked or a contact request.
    pub blocked: Blocked,

    /// Additional chat parameters stored in the database.
    pub param: Params,

    /// If location streaming is enabled in the chat.
    is_sending_locations: bool,

    /// Duration of the chat being muted.
    pub mute_duration: MuteDuration,

    /// If the chat is protected (verified).
    pub(crate) protected: ProtectionStatus,
}

pub enum Param {
    /// For messages
    File = b'f',

    /// For messages: original filename (as shown in chat)
    Filename = b'v',

    /// For messages: This name should be shown instead of contact.get_display_name()
    /// (used if this is a mailinglist
    /// or explicitly set using set_override_sender_name(), eg. by bots)
    OverrideSenderDisplayname = b'O',

    /// For Messages
    Width = b'w',

    /// For Messages
    Height = b'h',

    /// For Messages
    Duration = b'd',

    /// For Messages
    MimeType = b'm',

    /// For Messages: HTML to be written to the database and to be send.
    /// `SendHtml` param is not used for received messages.
    /// Use `MsgId::get_html()` to get HTML of received messages.
    SendHtml = b'T',

    /// For Messages: message is encrypted, outgoing: guarantee E2EE or the message is not send
    GuaranteeE2ee = b'c',

    /// For Messages: quoted message is encrypted.
    ///
    /// If this message is sent unencrypted, quote text should be replaced.
    ProtectQuote = b'0',

    /// For Messages: decrypted with validation errors or without mutual set, if neither
    /// 'c' nor 'e' are preset, the messages is only transport encrypted.
    ErroneousE2ee = b'e',

    /// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
    ForcePlaintext = b'u',

    /// For Messages: do not include Autocrypt header.
    SkipAutocrypt = b'o',

    /// For Messages
    WantsMdn = b'r',

    /// For Messages: the message is a reaction.
    Reaction = b'x',

    /// For Chats: the timestamp of the last reaction.
    LastReactionTimestamp = b'y',

    /// For Chats: Message ID of the last reaction.
    LastReactionMsgId = b'Y',

    /// For Chats: Contact ID of the last reaction.
    LastReactionContactId = b'1',

    /// For Messages: a message with "Auto-Submitted: auto-generated" header ("bot").
    Bot = b'b',

    /// For Messages: unset or 0=not forwarded,
    /// 1=forwarded from unknown msg_id, >9 forwarded from msg_id
    Forwarded = b'a',

    /// For Messages: quoted text.
    Quote = b'q',

    /// For Messages
    Cmd = b'S',

    /// For Messages
    Arg = b'E',

    /// For Messages
    Arg2 = b'F',

    /// `Secure-Join-Fingerprint` header for `{vc,vg}-request-with-auth` messages.
    Arg3 = b'G',

    /// Deprecated `Secure-Join-Group` header for messages.
    Arg4 = b'H',

    /// For Messages
    AttachGroupImage = b'A',

    /// For Messages
    WebrtcRoom = b'V',

    /// For Messages: space-separated list of messaged IDs of forwarded copies.
    ///
    /// This is used when a [crate::message::Message] is in the
    /// [crate::message::MessageState::OutPending] state but is already forwarded.
    /// In this case the forwarded messages are written to the
    /// database and their message IDs are added to this parameter of
    /// the original message, which is also saved in the database.
    /// When the original message is then finally sent this parameter
    /// is used to also send all the forwarded messages.
    PrepForwards = b'P',

    /// For Messages
    SetLatitude = b'l',

    /// For Messages
    SetLongitude = b'n',

    /// For Groups
    ///
    /// An unpromoted group has not had any messages sent to it and thus only exists on the
    /// creator's device.  Any changes made to an unpromoted group do not need to send
    /// system messages to the group members to update them of the changes.  Once a message
    /// has been sent to a group it is promoted and group changes require sending system
    /// messages to all members.
    Unpromoted = b'U',

    /// For Groups and Contacts
    ProfileImage = b'i',

    /// For Chats
    /// Signals whether the chat is the `saved messages` chat
    Selftalk = b'K',

    /// For Chats: On sending a new message we set the subject to `Re: <last subject>`.
    /// Usually we just use the subject of the parent message, but if the parent message
    /// is deleted, we use the LastSubject of the chat.
    LastSubject = b't',

    /// For Chats
    Devicetalk = b'D',

    /// For Chats: If this is a mailing list chat, contains the List-Post address.
    /// None if there simply is no `List-Post` header in the mailing list.
    /// Some("") if the mailing list is using multiple different List-Post headers.
    ///
    /// The List-Post address is the email address where the user can write to in order to
    /// post something to the mailing list.
    ListPost = b'p',

    /// For Contacts: If this is the List-Post address of a mailing list, contains
    /// the List-Id of the mailing list (which is also used as the group id of the chat).
    ListId = b's',

    /// For Contacts: timestamp of status (aka signature or footer) update.
    StatusTimestamp = b'j',

    /// For Contacts and Chats: timestamp of avatar update.
    AvatarTimestamp = b'J',

    /// For Chats: timestamp of status/signature/footer update.
    EphemeralSettingsTimestamp = b'B',

    /// For Chats: timestamp of subject update.
    SubjectTimestamp = b'C',

    /// For Chats: timestamp of group name update.
    GroupNameTimestamp = b'g',

    /// For Chats: timestamp of member list update.
    MemberListTimestamp = b'k',

    /// For Webxdc Message Instances: Current document name
    WebxdcDocument = b'R',

    /// For Webxdc Message Instances: timestamp of document name update.
    WebxdcDocumentTimestamp = b'W',

    /// For Webxdc Message Instances: Current summary
    WebxdcSummary = b'N',

    /// For Webxdc Message Instances: timestamp of summary update.
    WebxdcSummaryTimestamp = b'Q',

    /// For Webxdc Message Instances: Webxdc is an integration, see init_webxdc_integration()
    WebxdcIntegration = b'3',

    /// For Webxdc Message Instances: Chat to integrate the Webxdc for.
    WebxdcIntegrateFor = b'2',

    /// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
    ForceSticker = b'X',
    // 'L' was defined as ProtectionSettingsTimestamp for Chats, however, never used in production.
}

pub struct ChatId(u32);

use std::cmp;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
    self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
    DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
};
use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
use crate::log::LogExt;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::securejoin::BobState;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
    buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
    create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
    smeared_time, time, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::WEBXDC_SUFFIX;
use CantSendReason::*;
use super::*;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager};
use strum::IntoEnumIterator;
use tokio::fs;


* 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().
 * @return 1=success, 0=error
 */
int dc_set_chat_name(dc_context_t* context, uint32_t chat_id, const char* new_name)
{
	/* the function only sets the names of group chats; normal chats get their names from the contacts */
	int        success = 0;
	dc_chat_t* chat = dc_chat_new(context);
	dc_msg_t*  msg = dc_msg_new_untyped(context);
	char*      q3 = NULL;

	if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || new_name==NULL || new_name[0]==0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) {
		goto cleanup;
	}

	if (0==real_group_exists(context, chat_id)
	 || 0==dc_chat_load_from_db(chat, chat_id)) {
		goto cleanup;
	}

	if (strcmp(chat->name, new_name)==0) {
		success = 1;
		goto cleanup; /* name not modified */
	}

	if (!IS_SELF_IN_GROUP) {
		dc_log_event(context, DC_EVENT_ERROR_SELF_NOT_IN_GROUP, 0,
		             "Cannot set chat name; self not in group");
		goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */
	}

	q3 = sqlite3_mprintf("UPDATE chats SET name=%Q WHERE id=%i;", new_name, chat_id);
	if (!dc_sqlite3_execute(context->sql, q3)) {
		goto cleanup;
	}

	/* send a status mail to all group members, also needed for outself to allow multi-client */
	if (DO_SEND_STATUS_MAILS)
	{
		msg->type = DC_MSG_TEXT;
		msg->text = dc_stock_system_msg(context, DC_STR_MSGGRPNAME, chat->name, new_name, DC_CONTACT_ID_SELF);
		dc_param_set_int(msg->param, DC_PARAM_CMD,     DC_CMD_GROUPNAME_CHANGED);
		dc_param_set    (msg->param, DC_PARAM_CMD_ARG, chat->name);
		msg->id = dc_send_msg(context, chat_id, msg);
		context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id);
	}
	context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0);

	success = 1;

cleanup:
	sqlite3_free(q3);
	dc_chat_unref(chat);
	dc_msg_unref(msg);
	return success;
}

pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
    rename_ex(context, Sync, chat_id, new_name).await
}

async fn rename_ex(
    context: &Context,
    mut sync: sync::Sync,
    chat_id: ChatId,
    new_name: &str,
) -> Result<()> {
    let new_name = improve_single_line_input(new_name);
    /* the function only sets the names of group chats; normal chats get their names from the contacts */
    let mut success = false;

    ensure!(!new_name.is_empty(), "Invalid name");
    ensure!(!chat_id.is_special(), "Invalid chat ID");

    let chat = Chat::load_from_db(context, chat_id).await?;
    let mut msg = Message::default();

    if chat.typ == Chattype::Group
        || chat.typ == Chattype::Mailinglist
        || chat.typ == Chattype::Broadcast
    {
        if chat.name == new_name {
            success = true;
        } else if !chat.is_self_in_chat(context).await? {
            context.emit_event(EventType::ErrorSelfNotInGroup(
                "Cannot set chat name; self not in group".into(),
            ));
        } else {
            context
                .sql
                .execute(
                    "UPDATE chats SET name=? WHERE id=?;",
                    (new_name.to_string(), chat_id),
                )
                .await?;
            if chat.is_promoted()
                && !chat.is_mailing_list()
                && chat.typ != Chattype::Broadcast
                && improve_single_line_input(&chat.name) != new_name
            {
                msg.viewtype = Viewtype::Text;
                msg.text =
                    stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await;
                msg.param.set_cmd(SystemMessage::GroupNameChanged);
                if !chat.name.is_empty() {
                    msg.param.set(Param::Arg, &chat.name);
                }
                msg.id = send_msg(context, chat_id, &mut msg).await?;
                context.emit_msgs_changed(chat_id, msg.id);
                sync = Nosync;
            }
            context.emit_event(EventType::ChatModified(chat_id));
            chatlist_events::emit_chatlist_item_changed(context, chat_id);
            success = true;
        }
    }

    if !success {
        bail!("Failed to set name");
    }
    if sync.into() && chat.name != new_name {
        let sync_name = new_name.to_string();
        chat.sync(context, SyncAction::Rename(sync_name))
            .await
            .log_err(context)
            .ok();
    }
    Ok(())
}

pub struct Context {
    pub(crate) inner: Arc<InnerContext>,
}

pub struct ChatId(u32);


* 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.
 * @return 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.
 */
dc_msg_t* dc_get_draft(dc_context_t* context, uint32_t chat_id)
{
	uint32_t  draft_msg_id = 0;
	dc_msg_t* draft_msg = NULL;

	if (context==NULL || context->magic!=DC_CONTEXT_MAGIC
	 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) {
		return NULL;
	}

	draft_msg_id = get_draft_msg_id(context, chat_id);
	if (draft_msg_id==0) {
		return NULL;
	}

	draft_msg = dc_msg_new_untyped(context);
	if (!dc_msg_load_from_db(draft_msg, context, draft_msg_id)) {
		dc_msg_unref(draft_msg);
		return NULL;
	}

	return draft_msg;
}

pub async fn get_draft(self, context: &Context) -> Result<Option<Message>> {
        if self.is_special() {
            return Ok(None);
        }
        match self.get_draft_msg_id(context).await? {
            Some(draft_msg_id) => {
                let msg = Message::load_from_db(context, draft_msg_id).await?;
                Ok(Some(msg))
            }
            None => Ok(None),
        }
    }

async fn get_draft_msg_id(self, context: &Context) -> Result<Option<MsgId>> {
        let msg_id: Option<MsgId> = context
            .sql
            .query_get_value(
                "SELECT id FROM msgs WHERE chat_id=? AND state=?;",
                (self, MessageState::OutDraft),
            )
            .await?;
        Ok(msg_id)
    }

pub fn is_special(self) -> bool {
        (0..=DC_CHAT_ID_LAST_SPECIAL.0).contains(&self.0)
    }

pub struct Context {
    pub(crate) inner: Arc<InnerContext>,
}

pub struct ChatId(u32);

pub struct Message {
    /// Message ID.
    pub(crate) id: MsgId,

    /// `From:` contact ID.
    pub(crate) from_id: ContactId,

    /// ID of the first contact in the `To:` header.
    pub(crate) to_id: ContactId,

    /// ID of the chat message belongs to.
    pub(crate) chat_id: ChatId,

    /// Type of the message.
    pub(crate) viewtype: Viewtype,

    /// State of the message.
    pub(crate) state: MessageState,
    pub(crate) download_state: DownloadState,

    /// Whether the message is hidden.
    pub(crate) hidden: bool,
    pub(crate) timestamp_sort: i64,
    pub(crate) timestamp_sent: i64,
    pub(crate) timestamp_rcvd: i64,
    pub(crate) ephemeral_timer: EphemeralTimer,
    pub(crate) ephemeral_timestamp: i64,
    pub(crate) text: String,

    /// Message subject.
    ///
    /// If empty, a default subject will be generated when sending.
    pub(crate) subject: String,

    /// `Message-ID` header value.
    pub(crate) rfc724_mid: String,

    /// `In-Reply-To` header value.
    pub(crate) in_reply_to: Option<String>,
    pub(crate) is_dc_message: MessengerMessage,
    pub(crate) mime_modified: bool,
    pub(crate) chat_blocked: Blocked,
    pub(crate) location_id: u32,
    pub(crate) error: Option<String>,
    pub(crate) param: Params,
}

pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
        let message = Self::load_from_db_optional(context, id)
            .await?
            .with_context(|| format!("Message {id} does not exist"))?;
        Ok(message)
    }