diff --git a/libpurple/purpleconversation.h b/libpurple/purpleconversation.h
--- a/libpurple/purpleconversation.h
+++ b/libpurple/purpleconversation.h
@@ -32,6 +32,17 @@
 
 #include "purpleversion.h"
 
+/**
+ * PURPLE_CONVERSATION_DOMAIN:
+ *
+ * A GError domain for PurpleConversation.
+ *
+ * Since: 3.0
+ */
+#define PURPLE_CONVERSATION_DOMAIN \
+	(g_quark_from_static_string("purple-conversatin")) \
+	PURPLE_AVAILABLE_MACRO_IN_3_0
+
 #define PURPLE_TYPE_CONVERSATION (purple_conversation_get_type())
 
 PURPLE_AVAILABLE_IN_ALL
@@ -326,6 +337,42 @@
 void purple_conversation_write_system_message(PurpleConversation *conversation, const char *message, PurpleMessageFlags flags);
 
 /**
+ * purple_conversation_send_message_async:
+ * @conversation: The conversation.
+ * @message: The message to send.
+ * @cancellable: (nullable): optional GCancellable object, %NULL to ignore.
+ * @callback: (nullable) (scope async): The callback to call after the
+ *            conversation has been created.
+ * @data: (nullable): Optional user data to pass to @callback.
+ *
+ * Sends @message to @conversation.
+ *
+ * This will lookup the account and protocol plugin that @conversation belongs
+ * to and call [method@ProtocolConversation.send_message_async] for @message.
+ *
+ * You will need to call [method@Conversation.send_message_finish] from
+ * @callback.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_conversation_send_message_async(PurpleConversation *conversation, PurpleMessage *message, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_conversation_send_message_finish:
+ * @conversation: The instance.
+ * @result: The result.
+ * @error: Return address for a #GError, or %NULL.
+ *
+ * Finishes a previous call to [method@Conversation.send_message_async].
+ *
+ * Returns: %TRUE on success, otherwise %FALSE with error set.
+ *
+ * Since: 3.0
+ */
+gboolean purple_conversation_send_message_finish(PurpleConversation *conversation, GAsyncResult *result, GError **error);
+
+/**
  * purple_conversation_send:
  * @conversation: The conversation.
  * @message: The message to send.
# HG changeset patch
# Node ID 3e0422d337d0
# Parent  44125b8e3b27
diff --git a/libpurple/purpleconversation.c b/libpurple/purpleconversation.c
--- a/libpurple/purpleconversation.c
+++ b/libpurple/purpleconversation.c
@@ -184,9 +184,9 @@
 }
 
 static void
-purple_conversation_send_message_async_cb(GObject *protocol,
-                                          GAsyncResult *result,
-                                          gpointer data)
+purple_conversation_send_message_async_old_cb(GObject *protocol,
+                                              GAsyncResult *result,
+                                              gpointer data)
 {
 	PurpleProtocolConversation *protocol_conversation = NULL;
 	PurpleMessage *message = data;
@@ -206,6 +206,54 @@
 }
 
 static void
+purple_conversation_send_message_async_cb(GObject *source,
+                                          GAsyncResult *result,
+                                          gpointer data)
+{
+	PurpleMessage *message = NULL;
+	PurpleProtocolConversation *protocol = NULL;
+	GError *error = NULL;
+	GTask *task = data;
+	gboolean success = FALSE;
+
+	/* task and result share a cancellable, so we just need to clear task to
+	 * make sure its callback gets called.
+	 */
+	if(g_task_return_error_if_cancelled(G_TASK(task))) {
+		g_clear_object(&task);
+
+		return;
+	}
+
+	protocol = PURPLE_PROTOCOL_CONVERSATION(source);
+	message = g_task_get_task_data(G_TASK(task));
+
+	success = purple_protocol_conversation_send_message_finish(protocol,
+	                                                           result, &error);
+
+	if(!success) {
+		if(error == NULL) {
+			error = g_error_new(PURPLE_CONVERSATION_DOMAIN, 0,
+			                    "unknown error");
+		}
+
+		purple_message_set_error(message, error);
+		g_task_return_error(task, error);
+
+		g_clear_error(&error);
+	} else {
+		/* If the protocol didn't set delivered, set it now. */
+		if(!purple_message_get_delivered(message)) {
+			purple_message_set_delivered(message, TRUE);
+		}
+
+		g_task_return_boolean(task, TRUE);
+	}
+
+	g_clear_object(&task);
+}
+
+static void
 common_send(PurpleConversation *conversation, const char *message,
             PurpleMessageFlags msgflags)
 {
@@ -260,7 +308,7 @@
 		purple_protocol_conversation_send_message_async(protocol_conversation,
 		                                                conversation, msg,
 		                                                NULL,
-		                                                purple_conversation_send_message_async_cb,
+		                                                purple_conversation_send_message_async_old_cb,
 		                                                msg);
 
 		g_clear_pointer(&displayed, g_free);
@@ -1181,6 +1229,59 @@
 }
 
 void
+purple_conversation_send_message_async(PurpleConversation *conversation,
+                                       PurpleMessage *message,
+                                       GCancellable *cancellable,
+                                       GAsyncReadyCallback callback,
+                                       gpointer data)
+{
+	PurpleAccount *account = NULL;
+	PurpleProtocol *protocol = NULL;
+	GTask *task = NULL;
+
+	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
+	g_return_if_fail(PURPLE_IS_MESSAGE(message));
+
+	task = g_task_new(conversation, cancellable, callback, data);
+	g_task_set_source_tag(task, purple_conversation_send_message_async);
+	g_task_set_task_data(task, g_object_ref(message), g_object_unref);
+
+	account = purple_conversation_get_account(conversation);
+	protocol = purple_account_get_protocol(account);
+
+	if(!PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) {
+		g_task_return_new_error(task, PURPLE_CONVERSATION_DOMAIN, 0,
+		                        "protocol does not implement "
+		                        "PurpleProtocolConversation");
+
+		g_clear_object(&task);
+
+		return;
+	}
+
+	purple_protocol_conversation_send_message_async(PURPLE_PROTOCOL_CONVERSATION(protocol),
+	                                                conversation,
+	                                                message,
+	                                                cancellable,
+	                                                purple_conversation_send_message_async_cb,
+	                                                task);
+}
+
+gboolean
+purple_conversation_send_message_finish(PurpleConversation *conversation,
+                                        GAsyncResult *result,
+                                        GError **error)
+{
+	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
+	g_return_val_if_fail(G_IS_ASYNC_RESULT(result), FALSE);
+
+	g_return_val_if_fail(g_task_get_source_tag(G_TASK(result)) !=
+	                     purple_conversation_send_message_async, FALSE);
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+void
 purple_conversation_send(PurpleConversation *conversation,
                          const char *message)
 {
diff --git a/libpurple/purplemessage.h b/libpurple/purplemessage.h
--- a/libpurple/purplemessage.h
+++ b/libpurple/purplemessage.h
@@ -369,7 +369,7 @@
 /**
  * purple_message_set_error:
  * @message: The instance.
- * @error: (nullable) (transfer full): The error to set.
+ * @error: (nullable) (transfer none): The error to set.
  *
  * Sets the error of @message to @error. Primarily this will be used for
  * delivery failure.
diff --git a/libpurple/purplemessage.c b/libpurple/purplemessage.c
--- a/libpurple/purplemessage.c
+++ b/libpurple/purplemessage.c
@@ -683,7 +683,7 @@
 
 	g_clear_error(&message->error);
 	if(error != NULL) {
-		g_propagate_error(&message->error, error);
+		message->error = g_error_copy(error);
 	}
 
 	g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_ERROR]);
