int dc_mimefactory_render(dc_mimefactory_t* factory)
{
	struct mailimf_fields* imf_fields = NULL;
	struct mailmime*       message = NULL;
	char*                  message_text = NULL;
	char*                  message_text2 = NULL;
	char*                  subject_str = NULL;
	int                    afwd_email = 0;
	int                    col = 0;
	int                    success = 0;
	int                    parts = 0;
	int                    e2ee_guaranteed = 0;
	int                    min_verified = DC_NOT_VERIFIED;
	int                    force_plaintext = 0; // 1=add Autocrypt-header (needed eg. for handshaking), 2=no Autocrypte-header (used for MDN)
	int                    do_gossip = 0;
	char*                  grpimage = NULL;
	dc_e2ee_helper_t       e2ee_helper;
	memset(&e2ee_helper, 0, sizeof(dc_e2ee_helper_t));

	if (factory==NULL || factory->loaded==DC_MF_NOTHING_LOADED || factory->out/*call empty() before*/) {
		set_error(factory, "Invalid use of mimefactory-object.");
		goto cleanup;
	}

	/* create basic mail
	 *************************************************************************/

	{
		struct mailimf_mailbox_list* from = mailimf_mailbox_list_new_empty();
		mailimf_mailbox_list_add(from, mailimf_mailbox_new(factory->from_displayname? dc_encode_header_words(factory->from_displayname) : NULL, dc_strdup(factory->from_addr)));

		struct mailimf_address_list* to = NULL;
		if (factory->recipients_names && factory->recipients_addr && clist_count(factory->recipients_addr)>0) {
			clistiter *iter1, *iter2;
			to = mailimf_address_list_new_empty();
			for (iter1=clist_begin(factory->recipients_names),iter2=clist_begin(factory->recipients_addr);  iter1!=NULL&&iter2!=NULL;  iter1=clist_next(iter1),iter2=clist_next(iter2)) {
				const char* name = clist_content(iter1);
				const char* addr = clist_content(iter2);
				mailimf_address_list_add(to, mailimf_address_new(MAILIMF_ADDRESS_MAILBOX, mailimf_mailbox_new(name? dc_encode_header_words(name) : NULL, dc_strdup(addr)), NULL));
			}
		}

		clist* references_list = NULL;
		if (factory->references && factory->references[0]) {
			references_list = dc_str_to_clist(factory->references, " ");
		}

		clist* in_reply_to_list = NULL;
		if (factory->in_reply_to && factory->in_reply_to[0]) {
			in_reply_to_list = dc_str_to_clist(factory->in_reply_to, " ");
		}

		imf_fields = mailimf_fields_new_with_data_all(mailimf_get_date(factory->timestamp), from,
			NULL /* sender */, NULL /* reply-to */,
			to, NULL /* cc */, NULL /* bcc */, dc_strdup(factory->rfc724_mid), in_reply_to_list,
			references_list /* references */,
			NULL /* subject set later */);

		/* Add a X-Mailer header. This is only informational for debugging and may be removed in the release.
		We do not rely on this header as it may be removed by MTAs. */
		mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("X-Mailer"),
			dc_mprintf("Delta Chat Core %s%s%s",
			DC_VERSION_STR,
			factory->context->os_name? "/" : "",
			factory->context->os_name? factory->context->os_name : "")));

		mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Version"), strdup("1.0"))); /* mark message as being sent by a messenger */

		if (factory->req_mdn) {
			/* we use "Chat-Disposition-Notification-To" as replies to "Disposition-Notification-To" are weird in many cases, are just freetext and/or do not follow any standard. */
			mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Disposition-Notification-To"), strdup(factory->from_addr)));
		}

		message = mailmime_new_message_data(NULL);
		mailmime_set_imf_fields(message, imf_fields);
	}

	if (factory->loaded==DC_MF_MSG_LOADED)
	{
		/* Render a normal message
		 *********************************************************************/

		dc_chat_t* chat = factory->chat;
		dc_msg_t*  msg  = factory->msg;

		struct mailmime* meta_part = NULL;
		char* placeholdertext = NULL;

		if (chat->type==DC_CHAT_TYPE_VERIFIED_GROUP) {
			mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Verified"), strdup("1")));
			force_plaintext   = 0;
			e2ee_guaranteed   = 1;
			min_verified      = DC_BIDIRECT_VERIFIED;
		}
		else {
			if ((force_plaintext = dc_param_get_int(factory->msg->param, DC_PARAM_FORCE_PLAINTEXT, 0))==0) {
				e2ee_guaranteed = dc_param_get_int(factory->msg->param, DC_PARAM_GUARANTEE_E2EE, 0);
			}
		}

		// beside key- and member-changes, force re-gossip every 48 hours
		#define AUTO_REGOSSIP (2*24*60*60)
		if (chat->gossiped_timestamp==0
		 || chat->gossiped_timestamp+AUTO_REGOSSIP < time(NULL) ) {
			do_gossip = 1;
		}

		/* build header etc. */
		int command = dc_param_get_int(msg->param, DC_PARAM_CMD, 0);
		if (DC_CHAT_TYPE_IS_MULTI(chat->type))
		{
			mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-ID"), dc_strdup(chat->grpid)));
			mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-Name"), dc_encode_header_words(chat->name)));

			if (command==DC_CMD_MEMBER_REMOVED_FROM_GROUP)
			{
				char* email_to_remove = dc_param_get(msg->param, DC_PARAM_CMD_ARG, NULL);
				if (email_to_remove) {
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-Member-Removed"), email_to_remove));
				}
			}
			else if (command==DC_CMD_MEMBER_ADDED_TO_GROUP)
			{
				do_gossip = 1;

				char* email_to_add = dc_param_get(msg->param, DC_PARAM_CMD_ARG, NULL);
				if (email_to_add) {
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-Member-Added"), email_to_add));
					grpimage = dc_param_get(chat->param, DC_PARAM_PROFILE_IMAGE, NULL);
				}

				if (dc_param_get_int(msg->param, DC_PARAM_CMD_ARG2, 0)&DC_FROM_HANDSHAKE) {
					dc_log_info(msg->context, 0, "sending secure-join message '%s' >>>>>>>>>>>>>>>>>>>>>>>>>", "vg-member-added");
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Secure-Join"), strdup("vg-member-added")));
				}
			}
			else if (command==DC_CMD_GROUPNAME_CHANGED)
			{
				mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-Name-Changed"), dc_param_get(msg->param, DC_PARAM_CMD_ARG, "")));
			}
			else if (command==DC_CMD_GROUPIMAGE_CHANGED)
			{
				grpimage = dc_param_get(msg->param, DC_PARAM_CMD_ARG, NULL);
				if (grpimage==NULL) {
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-Image"), dc_strdup("0")));
				}
			}
		}

		if (command==DC_CMD_LOCATION_STREAMING_ENABLED) {
			mailimf_fields_add(imf_fields, mailimf_field_new_custom(
				strdup("Chat-Content"),
				strdup("location-streaming-enabled")));
		}

		if (command==DC_CMD_AUTOCRYPT_SETUP_MESSAGE) {
			mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Autocrypt-Setup-Message"), strdup("v1")));
			placeholdertext = dc_stock_str(factory->context, DC_STR_AC_SETUP_MSG_BODY);
		}

		if (command==DC_CMD_SECUREJOIN_MESSAGE) {
			char* step = dc_param_get(msg->param, DC_PARAM_CMD_ARG, NULL);
			if (step) {
				dc_log_info(msg->context, 0, "sending secure-join message '%s' >>>>>>>>>>>>>>>>>>>>>>>>>", step);
				mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Secure-Join"), step/*mailimf takes ownership of string*/));

				char* param2 = dc_param_get(msg->param, DC_PARAM_CMD_ARG2, NULL);
				if (param2) {
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(
						(strcmp(step, "vg-request-with-auth")==0 || strcmp(step, "vc-request-with-auth")==0)?
							strdup("Secure-Join-Auth") : strdup("Secure-Join-Invitenumber"),
						param2/*mailimf takes ownership of string*/));
				}

				char* fingerprint = dc_param_get(msg->param, DC_PARAM_CMD_ARG3, NULL);
				if (fingerprint) {
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(
						strdup("Secure-Join-Fingerprint"),
						fingerprint/*mailimf takes ownership of string*/));
				}

				char* grpid = dc_param_get(msg->param, DC_PARAM_CMD_ARG4, NULL);
				if (grpid) {
					mailimf_fields_add(imf_fields, mailimf_field_new_custom(
						strdup("Secure-Join-Group"),
						grpid/*mailimf takes ownership of string*/));
				}
			}
		}

		if (grpimage)
		{
			dc_msg_t* meta = dc_msg_new_untyped(factory->context);
			meta->type = DC_MSG_IMAGE;
			dc_param_set(meta->param, DC_PARAM_FILE, grpimage);
			char* filename_as_sent = NULL;
			if ((meta_part=build_body_file(meta, "group-image", &filename_as_sent))!=NULL) {
				mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Group-Image"), filename_as_sent/*takes ownership*/));
			}
			dc_msg_unref(meta);
		}

		if (msg->type==DC_MSG_VOICE || msg->type==DC_MSG_AUDIO || msg->type==DC_MSG_VIDEO)
		{
			if (msg->type==DC_MSG_VOICE) {
				mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Voice-Message"), strdup("1")));
			}

			int duration_ms = dc_param_get_int(msg->param, DC_PARAM_DURATION, 0);
			if (duration_ms > 0) {
				mailimf_fields_add(imf_fields, mailimf_field_new_custom(strdup("Chat-Duration"), dc_mprintf("%i", (int)duration_ms)));
			}
		}

		/* add text part - we even add empty text and force a MIME-multipart-message as:
		- some Apps have problems with Non-text in the main part (eg. "Mail" from stock Android)
		- we can add "forward hints" this way
		- it looks better */
		afwd_email = dc_param_exists(msg->param, DC_PARAM_FORWARDED);
		char* fwdhint = NULL;
		if (afwd_email) {
			fwdhint = dc_strdup("----- Forwarded message -----" LINEEND "From: Delta Chat" LINEEND LINEEND); /* do not chage this! expected this way in the simplifier to detect forwarding! */
		}

		const char* final_text = NULL;
		if (placeholdertext) {
			final_text = placeholdertext;
		}
		else if (msg->text && msg->text[0]) {
			final_text = msg->text;
		}

		char* footer = factory->selfstatus;
		message_text = dc_mprintf("%s%s%s%s%s",
			fwdhint? fwdhint : "",
			final_text? final_text : "",
			(final_text&&footer&&footer[0])? (LINEEND LINEEND) : "",
			(footer&&footer[0])? ("-- " LINEEND)  : "",
			(footer&&footer[0])? footer       : "");
		struct mailmime* text_part = build_body_text(message_text);
		mailmime_smart_add_part(message, text_part);
		parts++;

		free(fwdhint);
		free(placeholdertext);

		/* add attachment part */
		if (DC_MSG_NEEDS_ATTACHMENT(msg->type)) {
			if (!is_file_size_okay(msg)) {
				char* error = dc_mprintf("Message exceeds the recommended %i MB.", DC_MSGSIZE_MAX_RECOMMENDED/1000/1000);
				set_error(factory, error);
				free(error);
				goto cleanup;
			}

			struct mailmime* file_part = build_body_file(msg, NULL, NULL);
			if (file_part) {
				mailmime_smart_add_part(message, file_part);
				parts++;
			}
		}

		if (parts==0) {
			set_error(factory, "Empty message.");
			goto cleanup;
		}

		if (meta_part) {
			mailmime_smart_add_part(message, meta_part); /* meta parts are only added if there are other parts */
			parts++;
		}

		if (dc_param_exists(msg->param, DC_PARAM_SET_LATITUDE)) {
			double latitude  = dc_param_get_float(msg->param, DC_PARAM_SET_LATITUDE, 0.0);
			double longitude = dc_param_get_float(msg->param, DC_PARAM_SET_LONGITUDE, 0.0);
			char* kml_file = dc_get_message_kml(msg->context, msg->timestamp_sort, latitude, longitude);
			if (kml_file) {
				struct mailmime_content* content_type = mailmime_content_new_with_str("application/vnd.google-earth.kml+xml");
				struct mailmime_fields* mime_fields = mailmime_fields_new_filename(MAILMIME_DISPOSITION_TYPE_ATTACHMENT,
					dc_strdup("message.kml"), MAILMIME_MECHANISM_8BIT);
				struct mailmime* kml_mime_part = mailmime_new_empty(content_type, mime_fields);
				mailmime_set_body_text(kml_mime_part, kml_file, strlen(kml_file));

				mailmime_smart_add_part(message, kml_mime_part);
				parts++;
			}
		}

		if (dc_is_sending_locations_to_chat(msg->context, msg->chat_id)) {
			uint32_t last_added_location_id = 0;
			char* kml_file = dc_get_location_kml(msg->context, msg->chat_id,
				&last_added_location_id);
			if (kml_file) {
				struct mailmime_content* content_type = mailmime_content_new_with_str("application/vnd.google-earth.kml+xml");
				struct mailmime_fields* mime_fields = mailmime_fields_new_filename(MAILMIME_DISPOSITION_TYPE_ATTACHMENT,
					dc_strdup("location.kml"), MAILMIME_MECHANISM_8BIT);
				struct mailmime* kml_mime_part = mailmime_new_empty(content_type, mime_fields);
				mailmime_set_body_text(kml_mime_part, kml_file, strlen(kml_file));

				mailmime_smart_add_part(message, kml_mime_part);
				parts++;

				if (!dc_param_exists(msg->param, DC_PARAM_SET_LATITUDE)) { // otherwise, the independent location is already filed
					factory->out_last_added_location_id = last_added_location_id;
				}
			}
		}
	}
	else if (factory->loaded==DC_MF_MDN_LOADED)
	{
		/* Render a MDN
		 *********************************************************************/

		struct mailmime* multipart = mailmime_multiple_new("multipart/report"); /* RFC 6522, this also requires the `report-type` parameter which is equal to the MIME subtype of the second body part of the multipart/report */
		struct mailmime_content* content = multipart->mm_content_type;
		clist_append(content->ct_parameters, mailmime_param_new_with_data("report-type", "disposition-notification")); /* RFC  */
		mailmime_add_part(message, multipart);

		/* first body part: always human-readable, always REQUIRED by RFC 6522 */
		char *p1 = NULL, *p2 = NULL;
		if (dc_param_get_int(factory->msg->param, DC_PARAM_GUARANTEE_E2EE, 0)) {
			p1 = dc_stock_str(factory->context, DC_STR_ENCRYPTEDMSG); /* we SHOULD NOT spread encrypted subjects, date etc. in potentially unencrypted MDNs */
		}
		else {
			p1 = dc_msg_get_summarytext(factory->msg, DC_APPROX_SUBJECT_CHARS);
		}
		p2 = dc_stock_str_repl_string(factory->context, DC_STR_READRCPT_MAILBODY, p1);
		message_text = dc_mprintf("%s" LINEEND, p2);
		free(p2);
		free(p1);

		struct mailmime* human_mime_part = build_body_text(message_text);
		mailmime_add_part(multipart, human_mime_part);

		/* second body part: machine-readable, always REQUIRED by RFC 6522 */
		message_text2 = dc_mprintf(
			"Reporting-UA: Delta Chat %s" LINEEND
			"Original-Recipient: rfc822;%s" LINEEND
			"Final-Recipient: rfc822;%s" LINEEND
			"Original-Message-ID: <%s>" LINEEND
			"Disposition: manual-action/MDN-sent-automatically; displayed" LINEEND, /* manual-action: the user has configured the MUA to send MDNs (automatic-action implies the receipts cannot be disabled) */
			DC_VERSION_STR,
			factory->from_addr,
			factory->from_addr,
			factory->msg->rfc724_mid);

		struct mailmime_content* content_type = mailmime_content_new_with_str("message/disposition-notification");
		struct mailmime_fields* mime_fields = mailmime_fields_new_encoding(MAILMIME_MECHANISM_8BIT);
		struct mailmime* mach_mime_part = mailmime_new_empty(content_type, mime_fields);
		mailmime_set_body_text(mach_mime_part, message_text2, strlen(message_text2));

		mailmime_add_part(multipart, mach_mime_part);

		/* currently, we do not send MDNs encrypted:
		- in a multi-device-setup that is not set up properly, MDNs would disturb the communication as they
		  are send automatically which may lead to spreading outdated Autocrypt headers.
		- they do not carry any information but the Message-ID
		- this save some KB
		- in older versions, we did not encrypt messages to ourself when they to to SMTP - however, if these messages
		  are forwarded for any reasons (eg. gmail always forwards to IMAP), we have no chance to decrypt them;
		  this issue is fixed with 0.9.4 */
		force_plaintext = DC_FP_NO_AUTOCRYPT_HEADER;
	}
	else
	{
		set_error(factory, "No message loaded.");
		goto cleanup;
	}

	/* Encrypt the message
	 *************************************************************************/

	if (factory->loaded==DC_MF_MDN_LOADED) {
		char* e = dc_stock_str(factory->context, DC_STR_READRCPT); subject_str = dc_mprintf(DC_CHAT_PREFIX " %s", e); free(e);
	}
	else {
		subject_str = get_subject(factory->chat, factory->msg, afwd_email);
	}

	struct mailimf_subject* subject = mailimf_subject_new(dc_encode_header_words(subject_str));
	mailimf_fields_add(imf_fields, mailimf_field_new(MAILIMF_FIELD_SUBJECT, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, subject, NULL, NULL, NULL));

	if (force_plaintext!=DC_FP_NO_AUTOCRYPT_HEADER) {
		dc_e2ee_encrypt(factory->context, factory->recipients_addr,
			force_plaintext, e2ee_guaranteed, min_verified,
			do_gossip, message, &e2ee_helper);

	}

	if (e2ee_helper.encryption_successfull) {
		factory->out_encrypted = 1;
		if (do_gossip) {
			factory->out_gossiped = 1;
		}
	}

	/* create the full mail and return */
	factory->out = mmap_string_new("");
	mailmime_write_mem(factory->out, &col, message);

	//{char* t4=dc_null_terminate(ret->str,ret->len); printf("MESSAGE:\n%s\n",t4);free(t4);}

	success = 1;

cleanup:
	if (message) {
		mailmime_free(message);
	}
	dc_e2ee_thanks(&e2ee_helper); // frees data referenced by "mailmime" but not freed by mailmime_free()
	free(message_text);           // mailmime_set_body_text() does not take ownership of "text"
	free(message_text2);          //   - " --
	free(subject_str);
	free(grpimage);
	return success;
}

pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
        let mut headers: MessageHeaders = Default::default();

        let from = Address::new_mailbox_with_name(
            self.from_displayname.to_string(),
            self.from_addr.clone(),
        );

        let undisclosed_recipients = match &self.loaded {
            Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
            Loaded::Mdn { .. } => false,
        };

        let mut to = Vec::new();
        if undisclosed_recipients {
            to.push(Address::new_group(
                "hidden-recipients".to_string(),
                Vec::new(),
            ));
        } else {
            let email_to_remove =
                if self.msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
                    self.msg.param.get(Param::Arg)
                } else {
                    None
                };

            for (name, addr) in &self.recipients {
                if let Some(email_to_remove) = email_to_remove {
                    if email_to_remove == addr {
                        continue;
                    }
                }

                if name.is_empty() {
                    to.push(Address::new_mailbox(addr.clone()));
                } else {
                    to.push(Address::new_mailbox_with_name(
                        name.to_string(),
                        addr.clone(),
                    ));
                }
            }

            if to.is_empty() {
                to.push(from.clone());
            }
        }

        // Start with Internet Message Format headers in the order of the standard example
        // <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
        let from_header = Header::new_with_value("From".into(), vec![from]).unwrap();
        headers.unprotected.push(from_header.clone());
        headers.protected.push(from_header);

        if let Some(sender_displayname) = &self.sender_displayname {
            let sender =
                Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
            headers
                .unprotected
                .push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
        }
        headers
            .unprotected
            .push(Header::new_with_value("To".into(), to).unwrap());

        let subject_str = self.subject_str(context).await?;
        let encoded_subject = if subject_str
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == ' ')
        // We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
        // but we do not want to encode all subjects just because they contain a space.
        {
            subject_str.clone()
        } else {
            encode_words(&subject_str)
        };
        headers
            .protected
            .push(Header::new("Subject".into(), encoded_subject));

        let date = chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp, 0)
            .unwrap()
            .to_rfc2822();
        headers.unprotected.push(Header::new("Date".into(), date));

        let rfc724_mid = match self.loaded {
            Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
            Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
        };
        let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
        let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue);
        headers.unprotected.push(rfc724_mid_header.clone());
        headers.hidden.push(rfc724_mid_header);

        // Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
        if !self.in_reply_to.is_empty() {
            headers
                .unprotected
                .push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
        }
        if !self.references.is_empty() {
            headers
                .unprotected
                .push(Header::new("References".into(), self.references.clone()));
        }

        // Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
        if let Loaded::Mdn { .. } = self.loaded {
            headers.unprotected.push(Header::new(
                "Auto-Submitted".to_string(),
                "auto-replied".to_string(),
            ));
        } else if context.get_config_bool(Config::Bot).await? {
            headers.unprotected.push(Header::new(
                "Auto-Submitted".to_string(),
                "auto-generated".to_string(),
            ));
        }

        if let Loaded::Message { chat } = &self.loaded {
            if chat.typ == Chattype::Broadcast {
                let encoded_chat_name = encode_words(&chat.name);
                headers.protected.push(Header::new(
                    "List-ID".into(),
                    format!("{encoded_chat_name} <{}>", chat.grpid),
                ));
            }
        }

        // Non-standard headers.
        headers
            .unprotected
            .push(Header::new("Chat-Version".to_string(), "1.0".to_string()));

        if self.req_mdn {
            // we use "Chat-Disposition-Notification-To"
            // because replies to "Disposition-Notification-To" are weird in many cases
            // eg. are just freetext and/or do not follow any standard.
            headers.protected.push(Header::new(
                "Chat-Disposition-Notification-To".into(),
                self.from_addr.clone(),
            ));
        }

        let verified = self.verified();
        let grpimage = self.grpimage();
        let force_plaintext = self.should_force_plaintext();
        let skip_autocrypt = self.should_skip_autocrypt();
        let e2ee_guaranteed = self.is_e2ee_guaranteed();
        let encrypt_helper = EncryptHelper::new(context).await?;

        if !skip_autocrypt {
            // unless determined otherwise we add the Autocrypt header
            let aheader = encrypt_helper.get_aheader().to_string();
            headers
                .unprotected
                .push(Header::new("Autocrypt".into(), aheader));
        }

        let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
        if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
            headers.protected.push(Header::new(
                "Ephemeral-Timer".to_string(),
                duration.to_string(),
            ));
        }

        // MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
        // Content-Type
        headers
            .unprotected
            .push(Header::new("MIME-Version".into(), "1.0".into()));

        let mut is_gossiped = false;

        let peerstates = self.peerstates_for_recipients(context).await?;
        let should_encrypt =
            encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
        let is_encrypted = should_encrypt && !force_plaintext;

        let (main_part, parts) = match self.loaded {
            Loaded::Message { .. } => {
                self.render_message(context, &mut headers, &grpimage, is_encrypted)
                    .await?
            }
            Loaded::Mdn { .. } => (self.render_mdn(context).await?, Vec::new()),
        };

        let message = if parts.is_empty() {
            // Single part, render as regular message.
            main_part
        } else {
            // Multiple parts, render as multipart.
            let part_holder = if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
                PartBuilder::new().header((
                    "Content-Type".to_string(),
                    "multipart/report; report-type=multi-device-sync".to_string(),
                ))
            } else if self.msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
                PartBuilder::new().header((
                    "Content-Type".to_string(),
                    "multipart/report; report-type=status-update".to_string(),
                ))
            } else {
                PartBuilder::new().message_type(MimeMultipartType::Mixed)
            };

            parts
                .into_iter()
                .fold(part_holder.child(main_part.build()), |message, part| {
                    message.child(part.build())
                })
        };

        let get_content_type_directives_header = || {
            (
                "Content-Type-Deltachat-Directives".to_string(),
                "protected-headers=\"v1\"".to_string(),
            )
        };
        let outer_message = if is_encrypted {
            // Store protected headers in the inner message.
            let message = headers
                .protected
                .into_iter()
                .fold(message, |message, header| message.header(header));

            // Add hidden headers to encrypted payload.
            let mut message = headers
                .hidden
                .into_iter()
                .fold(message, |message, header| message.header(header));

            // Add gossip headers in chats with multiple recipients
            let multiple_recipients =
                peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
            if self.should_do_gossip(context, multiple_recipients).await? {
                for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
                    if let Some(header) = peerstate.render_gossip_header(verified) {
                        message = message.header(Header::new("Autocrypt-Gossip".into(), header));
                        is_gossiped = true;
                    }
                }
            }

            // Set the appropriate Content-Type for the inner message.
            let mut existing_ct = message
                .get_header("Content-Type".to_string())
                .and_then(|h| h.get_value::<String>().ok())
                .unwrap_or_else(|| "text/plain; charset=utf-8;".to_string());

            if !existing_ct.ends_with(';') {
                existing_ct += ";";
            }
            let message = message.header(get_content_type_directives_header());

            // Set the appropriate Content-Type for the outer message
            let outer_message = PartBuilder::new().header((
                "Content-Type".to_string(),
                "multipart/encrypted; protocol=\"application/pgp-encrypted\"".to_string(),
            ));

            if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
                info!(
                    context,
                    "mimefactory: unencrypted message mime-body:\n{}",
                    message.clone().build().as_string(),
                );
            }

            // Disable compression for SecureJoin to ensure
            // there are no compression side channels
            // leaking information about the tokens.
            let compress = self.msg.param.get_cmd() != SystemMessage::SecurejoinMessage;
            let encrypted = encrypt_helper
                .encrypt(context, verified, message, peerstates, compress)
                .await?;

            outer_message
                .child(
                    // Autocrypt part 1
                    PartBuilder::new()
                        .content_type(&"application/pgp-encrypted".parse::<mime::Mime>().unwrap())
                        .header(("Content-Description", "PGP/MIME version identification"))
                        .body("Version: 1\r\n")
                        .build(),
                )
                .child(
                    // Autocrypt part 2
                    PartBuilder::new()
                        .content_type(
                            &"application/octet-stream; name=\"encrypted.asc\""
                                .parse::<mime::Mime>()
                                .unwrap(),
                        )
                        .header(("Content-Description", "OpenPGP encrypted message"))
                        .header(("Content-Disposition", "inline; filename=\"encrypted.asc\";"))
                        .body(encrypted)
                        .build(),
                )
                .header(("Subject".to_string(), "...".to_string()))
        } else if matches!(self.loaded, Loaded::Mdn { .. }) {
            // Never add outer multipart/mixed wrapper to MDN
            // as multipart/report Content-Type is used to recognize MDNs
            // by Delta Chat receiver and Chatmail servers
            // allowing them to be unencrypted and not contain Autocrypt header
            // without resetting Autocrypt encryption or triggering Chatmail filter
            // that normally only allows encrypted mails.

            // Hidden headers are dropped.

            // Store protected headers in the outer message.
            let message = headers
                .protected
                .iter()
                .fold(message, |message, header| message.header(header.clone()));

            let protected: HashSet<Header> = HashSet::from_iter(headers.protected.into_iter());
            for h in headers.unprotected.split_off(0) {
                if !protected.contains(&h) {
                    headers.unprotected.push(h);
                }
            }

            message
        } else {
            // Store hidden headers in the inner unencrypted message.
            let message = headers
                .hidden
                .into_iter()
                .fold(message, |message, header| message.header(header));
            let message = PartBuilder::new()
                .message_type(MimeMultipartType::Mixed)
                .child(message.build());

            // Store protected headers in the outer message.
            let message = headers
                .protected
                .iter()
                .fold(message, |message, header| message.header(header.clone()));

            if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
                let protected: HashSet<Header> = HashSet::from_iter(headers.protected.into_iter());
                for h in headers.unprotected.split_off(0) {
                    if !protected.contains(&h) {
                        headers.unprotected.push(h);
                    }
                }
                message
            } else {
                let message = message.header(get_content_type_directives_header());
                let (payload, signature) = encrypt_helper.sign(context, message).await?;
                PartBuilder::new()
                    .header((
                        "Content-Type",
                        "multipart/signed; protocol=\"application/pgp-signature\"",
                    ))
                    .child(payload)
                    .child(
                        PartBuilder::new()
                            .content_type(
                                &"application/pgp-signature; name=\"signature.asc\""
                                    .parse::<mime::Mime>()
                                    .unwrap(),
                            )
                            .header(("Content-Description", "OpenPGP digital signature"))
                            .header(("Content-Disposition", "attachment; filename=\"signature\";"))
                            .body(signature)
                            .build(),
                    )
            }
        };

        // Store the unprotected headers on the outer message.
        let outer_message = headers
            .unprotected
            .into_iter()
            .fold(outer_message, |message, header| message.header(header));

        if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
            info!(
                context,
                "mimefactory: outgoing message mime-body:\n{}",
                outer_message.clone().build().as_string(),
            );
        }

        let MimeFactory {
            last_added_location_id,
            ..
        } = self;

        Ok(RenderedEmail {
            message: outer_message.build().as_string(),
            // envelope: Envelope::new,
            is_encrypted,
            is_gossiped,
            last_added_location_id,
            sync_ids_to_delete: self.sync_ids_to_delete,
            rfc724_mid,
            subject: subject_str,
        })
    }

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

async fn render_mdn(&mut self, context: &Context) -> Result<PartBuilder> {
        // RFC 6522, this also requires the `report-type` parameter which is equal
        // to the MIME subtype of the second body part of the multipart/report
        //
        // currently, we do not send MDNs encrypted:
        // - in a multi-device-setup that is not set up properly, MDNs would disturb the communication as they
        //   are send automatically which may lead to spreading outdated Autocrypt headers.
        // - they do not carry any information but the Message-ID
        // - this save some KB
        // - in older versions, we did not encrypt messages to ourself when they to to SMTP - however, if these messages
        //   are forwarded for any reasons (eg. gmail always forwards to IMAP), we have no chance to decrypt them;
        //   this issue is fixed with 0.9.4

        let additional_msg_ids = match &self.loaded {
            Loaded::Message { .. } => bail!("Attempt to render a message as MDN"),
            Loaded::Mdn {
                additional_msg_ids, ..
            } => additional_msg_ids,
        };

        let mut message = PartBuilder::new().header((
            "Content-Type".to_string(),
            "multipart/report; report-type=disposition-notification".to_string(),
        ));

        // first body part: always human-readable, always REQUIRED by RFC 6522
        let p1 = if 0
            != self
                .msg
                .param
                .get_int(Param::GuaranteeE2ee)
                .unwrap_or_default()
        {
            stock_str::encrypted_msg(context).await
        } else {
            self.msg
                .get_summary(context, None)
                .await?
                .truncated_text(32)
                .to_string()
        };
        let p2 = stock_str::read_rcpt_mail_body(context, &p1).await;
        let message_text = format!("{}\r\n", format_flowed(&p2));
        let text_part = PartBuilder::new().header((
            "Content-Type".to_string(),
            "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
        ));
        let text_part = self.add_message_text(text_part, message_text);
        message = message.child(text_part.build());

        // second body part: machine-readable, always REQUIRED by RFC 6522
        let message_text2 = format!(
            "Original-Recipient: rfc822;{}\r\n\
             Final-Recipient: rfc822;{}\r\n\
             Original-Message-ID: <{}>\r\n\
             Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
            self.from_addr, self.from_addr, self.msg.rfc724_mid
        );

        let extension_fields = if additional_msg_ids.is_empty() {
            "".to_string()
        } else {
            "Additional-Message-IDs: ".to_string()
                + &additional_msg_ids
                    .iter()
                    .map(|mid| render_rfc724_mid(mid))
                    .collect::<Vec<String>>()
                    .join(" ")
                + "\r\n"
        };

        message = message.child(
            PartBuilder::new()
                .content_type(&"message/disposition-notification".parse().unwrap())
                .body(message_text2 + &extension_fields)
                .build(),
        );

        Ok(message)
    }

pub(crate) fn create_outgoing_rfc724_mid() -> String {
    format!("Mr.{}.{}@localhost", create_id(), create_id())
}

fn encode_words(word: &str) -> String {
    encoded_words::encode(word, None, encoded_words::EncodingFlag::Shortest, None)
}

fn render_rfc724_mid(rfc724_mid: &str) -> String {
    let rfc724_mid = rfc724_mid.trim().to_string();

    if rfc724_mid.chars().next().unwrap_or_default() == '<' {
        rfc724_mid
    } else {
        format!("<{rfc724_mid}>")
    }
}

pub enum Chattype {
    /// 1:1 chat.
    Single = 100,

    /// Group chat.
    Group = 120,

    /// Mailing list.
    Mailinglist = 140,

    /// Broadcast list.
    Broadcast = 160,
}

pub enum Loaded {
    Message { chat: Chat },
    Mdn { additional_msg_ids: Vec<String> },
}

pub enum SystemMessage {
    /// Unknown type of system message.
    #[default]
    Unknown = 0,

    /// Group name changed.
    GroupNameChanged = 2,

    /// Group avatar changed.
    GroupImageChanged = 3,

    /// Member was added to the group.
    MemberAddedToGroup = 4,

    /// Member was removed from the group.
    MemberRemovedFromGroup = 5,

    /// Autocrypt Setup Message.
    AutocryptSetupMessage = 6,

    /// Secure-join message.
    SecurejoinMessage = 7,

    /// Location streaming is enabled.
    LocationStreamingEnabled = 8,

    /// Location-only message.
    LocationOnly = 9,

    /// Chat ephemeral message timer is changed.
    EphemeralTimerChanged = 10,

    /// "Messages are guaranteed to be end-to-end encrypted from now on."
    ChatProtectionEnabled = 11,

    /// "%1$s sent a message from another device."
    ChatProtectionDisabled = 12,

    /// Message can't be sent because of `Invalid unencrypted mail to <>`
    /// which is sent by chatmail servers.
    InvalidUnencryptedMail = 13,

    /// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
    /// to complete.
    SecurejoinWait = 14,

    /// 1:1 chats info message telling that SecureJoin is still running, but the user may already
    /// send messages.
    SecurejoinWaitTimeout = 15,

    /// Self-sent-message that contains only json used for multi-device-sync;
    /// if possible, we attach that to other messages as for locations.
    MultiDeviceSync = 20,

    /// Sync message that contains a json payload
    /// sent to the other webxdc instances
    /// These messages are not shown in the chat.
    WebxdcStatusUpdate = 30,

    /// Webxdc info added with `info` set in `send_webxdc_status_update()`.
    WebxdcInfoMessage = 32,

    /// This message contains a users iroh node address.
    IrohNodeAddr = 40,
}

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.
}

use deltachat_contact_tools::ContactAddress;
use mailparse::{addrparse_header, MailHeaderMap};
use std::str;
use super::*;
use crate::chat::{
        add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId,
        ProtectionStatus,
    };
use crate::chatlist::Chatlist;
use crate::constants;
use crate::contact::Origin;
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};


char* dc_msg_get_summarytext_by_raw(int type, const char* text, dc_param_t* param, int approx_characters, dc_context_t* context)
{
	/* get a summary text, result must be free()'d, never returns NULL. */
	char* ret = NULL;
	char* prefix = NULL;
	char* pathNfilename = NULL;
	char* label = NULL;
	char* value = NULL;
	int   append_text = 1;

	switch (type) {
		case DC_MSG_IMAGE:
			prefix = dc_stock_str(context, DC_STR_IMAGE);
			break;

		case DC_MSG_GIF:
			prefix = dc_stock_str(context, DC_STR_GIF);
			break;

		case DC_MSG_VIDEO:
			prefix = dc_stock_str(context, DC_STR_VIDEO);
			break;

		case DC_MSG_VOICE:
			prefix = dc_stock_str(context, DC_STR_VOICEMESSAGE);
			break;

		case DC_MSG_AUDIO:
		case DC_MSG_FILE:
			if (dc_param_get_int(param, DC_PARAM_CMD, 0)==DC_CMD_AUTOCRYPT_SETUP_MESSAGE) {
				prefix = dc_stock_str(context, DC_STR_AC_SETUP_MSG_SUBJECT);
				append_text = 0;
			}
			else {
				pathNfilename = dc_param_get(param, DC_PARAM_FILE, "ErrFilename");
				value = dc_get_filename(pathNfilename);
				label = dc_stock_str(context, type==DC_MSG_AUDIO? DC_STR_AUDIO : DC_STR_FILE);
				prefix = dc_mprintf("%s " DC_NDASH " %s", label, value);
			}
			break;

		default:
			if (dc_param_get_int(param, DC_PARAM_CMD, 0)==DC_CMD_LOCATION_ONLY) {
				prefix = dc_stock_str(context, DC_STR_LOCATION);
				append_text = 0;
			}
			break;
	}

	if (append_text && prefix && text && text[0]) {
		ret = dc_mprintf("%s " DC_NDASH " %s", prefix, text);
		dc_truncate_n_unwrap_str(ret, approx_characters, 1/*unwrap*/);
	}
	else if (append_text && text && text[0]) {
		ret = dc_strdup(text);
		dc_truncate_n_unwrap_str(ret, approx_characters, 1/*unwrap*/);
	}
	else {
		ret = prefix;
		prefix = NULL;
	}

	/* cleanup */
	free(prefix);
	free(pathNfilename);
	free(label);
	free(value);
	if (ret==NULL) {
		ret = dc_strdup(NULL);
	}
	return ret;
}

async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
        let (emoji, type_name, type_file, append_text);
        match self.viewtype {
            Viewtype::Image => {
                emoji = Some("📷");
                type_name = Some(stock_str::image(context).await);
                type_file = None;
                append_text = true;
            }
            Viewtype::Gif => {
                emoji = None;
                type_name = Some(stock_str::gif(context).await);
                type_file = None;
                append_text = true;
            }
            Viewtype::Sticker => {
                emoji = None;
                type_name = Some(stock_str::sticker(context).await);
                type_file = None;
                append_text = true;
            }
            Viewtype::Video => {
                emoji = Some("🎥");
                type_name = Some(stock_str::video(context).await);
                type_file = None;
                append_text = true;
            }
            Viewtype::Voice => {
                emoji = Some("🎤");
                type_name = Some(stock_str::voice_message(context).await);
                type_file = None;
                append_text = true;
            }
            Viewtype::Audio => {
                emoji = Some("🎵");
                type_name = Some(stock_str::audio(context).await);
                type_file = self.get_filename();
                append_text = true
            }
            Viewtype::File => {
                if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
                    emoji = None;
                    type_name = Some(stock_str::ac_setup_msg_subject(context).await);
                    type_file = None;
                    append_text = false;
                } else {
                    emoji = Some("📎");
                    type_name = Some(stock_str::file(context).await);
                    type_file = self.get_filename();
                    append_text = true
                }
            }
            Viewtype::VideochatInvitation => {
                emoji = None;
                type_name = Some(stock_str::videochat_invitation(context).await);
                type_file = None;
                append_text = false;
            }
            Viewtype::Webxdc => {
                emoji = None;
                type_name = None;
                type_file = Some(
                    self.get_webxdc_info(context)
                        .await
                        .map(|info| info.name)
                        .unwrap_or_else(|_| "ErrWebxdcName".to_string()),
                );
                append_text = true;
            }
            Viewtype::Vcard => {
                emoji = Some("👤");
                type_name = Some(stock_str::contact(context).await);
                type_file = None;
                append_text = true;
            }
            Viewtype::Text | Viewtype::Unknown => {
                emoji = None;
                if self.param.get_cmd() == SystemMessage::LocationOnly {
                    type_name = Some(stock_str::location(context).await);
                    type_file = None;
                    append_text = false;
                } else {
                    type_name = None;
                    type_file = None;
                    append_text = true;
                }
            }
        };

        let text = self.text.clone();

        let summary = if let Some(type_file) = type_file {
            if append_text && !text.is_empty() {
                format!("{type_file} – {text}")
            } else {
                type_file
            }
        } else if append_text && !text.is_empty() {
            if emoji.is_some() {
                text
            } else if let Some(type_name) = type_name {
                format!("{type_name} – {text}")
            } else {
                text
            }
        } else if let Some(type_name) = type_name {
            type_name
        } else {
            "".to_string()
        };

        let summary = if let Some(emoji) = emoji {
            format!("{emoji} {summary}")
        } else {
            summary
        };

        summary.split_whitespace().collect::<Vec<&str>>().join(" ")
    }

pub(crate) async fn voice_message(context: &Context) -> String {
    translated(context, StockMessage::VoiceMessage).await
}

pub(crate) async fn image(context: &Context) -> String {
    translated(context, StockMessage::Image).await
}

pub(crate) async fn video(context: &Context) -> String {
    translated(context, StockMessage::Video).await
}

pub(crate) async fn audio(context: &Context) -> String {
    translated(context, StockMessage::Audio).await
}

pub(crate) async fn file(context: &Context) -> String {
    translated(context, StockMessage::File).await
}

pub(crate) async fn gif(context: &Context) -> String {
    translated(context, StockMessage::Gif).await
}

pub(crate) async fn sticker(context: &Context) -> String {
    translated(context, StockMessage::Sticker).await
}

pub(crate) async fn location(context: &Context) -> String {
    translated(context, StockMessage::Location).await
}

pub(crate) async fn ac_setup_msg_subject(context: &Context) -> String {
    translated(context, StockMessage::AcSetupMsgSubject).await
}

pub(crate) async fn videochat_invitation(context: &Context) -> String {
    translated(context, StockMessage::VideochatInvitation).await
}

pub(crate) async fn contact(context: &Context) -> String {
    translated(context, StockMessage::Contact).await
}

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

pub fn get_cmd(&self) -> SystemMessage {
        self.get_int(Param::Cmd)
            .and_then(SystemMessage::from_i32)
            .unwrap_or_default()
    }

pub fn get_filename(&self) -> Option<String> {
        if let Some(name) = self.param.get(Param::Filename) {
            return Some(name.to_string());
        }
        self.param
            .get(Param::File)
            .and_then(|file| Path::new(file).file_name())
            .map(|name| name.to_string_lossy().to_string())
    }

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

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 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,
}

use std::borrow::Cow;
use std::fmt;
use std::str;
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use crate::stock_str::msg_reacted;
use crate::tools::truncate;
use anyhow::Result;
use super::*;
use crate::param::Param;
use crate::test_utils as test;