diff --git a/libpurple/purplemessage.h b/libpurple/purplemessage.h
--- a/libpurple/purplemessage.h
+++ b/libpurple/purplemessage.h
@@ -377,7 +377,8 @@
  * @message: The instance.
  * @error: (nullable) (transfer full): The error to set.
  *
- * Sets the error of @message to at @error.
+ * Sets the error of @message to @error. Primarily this will be used for
+ * delivery failure.
  *
  * Since: 3.0.0
  */
@@ -458,6 +459,67 @@
  */
 void purple_message_clear_attachments(PurpleMessage *message);
 
+/**
+ * purple_message_get_delivered:
+ * @message: The instance.
+ *
+ * Gets whether or not the message was delivered.
+ *
+ * > Note: If a protocol does not support delivery receipts this will always
+ * return %FALSE.
+ *
+ * If the protocol does support delivery receipts [property@Message:error] may
+ * be set as well if delivery failed.
+ *
+ * Returns: %TRUE if @message has been delivered and the protocol supports
+ *          delivery notifications, otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_message_get_delivered(PurpleMessage *message);
+
+/**
+ * purple_message_set_delivered:
+ * @message: The instance.
+ * @delivered: Whether or not the message was delivered.
+ *
+ * Sets whether or not a message was delivered.
+ *
+ * > Note: Setting this will also set [property@Message:delivered-at]. If
+ * @delivered is %TRUE it will be set to the current time, otherwise it will be
+ * unset.
+ *
+ * Since: 3.0.0.
+ */
+void purple_message_set_delivered(PurpleMessage *message, gboolean delivered);
+
+/**
+ * purple_message_get_delivered_at:
+ * @message: The instance.
+ *
+ * Gets the time that @message was delivered if it was delivered, otherwise
+ * %NULL.
+ *
+ * Returns: (transfer none) (nullable): The delivery time of @message.
+ *
+ * Since: 3.0.0
+ */
+GDateTime *purple_message_get_delivered_at(PurpleMessage *message);
+
+/**
+ * purple_message_set_delivered_at:
+ * @message: The instance.
+ * @datetime: (nullable): The time of delivery.
+ *
+ * Sets the delivery time of @message to @datetime.
+ *
+ * > Note: Setting this will also set [property@Message:delivered]. If
+ * @datetime is %NULL it will be set to %FALSE, otherwise %TRUE.
+ *
+ * Since: 3.0.0
+ */
+void purple_message_set_delivered_at(PurpleMessage *message, GDateTime *datetime);
+
 G_END_DECLS
 
 #endif /* PURPLE_MESSAGE_H */
# HG changeset patch
# Node ID 84696a1adeb7
# Parent  2dce52f93848
diff --git a/libpurple/purplemessage.c b/libpurple/purplemessage.c
--- a/libpurple/purplemessage.c
+++ b/libpurple/purplemessage.c
@@ -44,6 +44,9 @@
 
 	GError *error;
 
+	gboolean delivered;
+	GDateTime *delivered_at;
+
 	GHashTable *attachments;
 };
 
@@ -59,6 +62,8 @@
 	PROP_TIMESTAMP,
 	PROP_FLAGS,
 	PROP_ERROR,
+	PROP_DELIVERED,
+	PROP_DELIVERED_AT,
 	N_PROPERTIES
 };
 static GParamSpec *properties[N_PROPERTIES];
@@ -125,6 +130,12 @@
 		case PROP_ERROR:
 			g_value_set_boxed(value, purple_message_get_error(message));
 			break;
+		case PROP_DELIVERED:
+			g_value_set_boolean(value, purple_message_get_delivered(message));
+			break;
+		case PROP_DELIVERED_AT:
+			g_value_set_boxed(value, purple_message_get_delivered_at(message));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
 			break;
@@ -169,6 +180,12 @@
 		case PROP_ERROR:
 			purple_message_set_error(message, g_value_get_boxed(value));
 			break;
+		case PROP_DELIVERED:
+			purple_message_set_delivered(message, g_value_get_boolean(value));
+			break;
+		case PROP_DELIVERED_AT:
+			purple_message_set_delivered_at(message, g_value_get_boxed(value));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
 			break;
@@ -192,6 +209,8 @@
 		g_date_time_unref(message->timestamp);
 	}
 
+	g_clear_pointer(&message->delivered_at, g_date_time_unref);
+
 	g_hash_table_destroy(message->attachments);
 
 	G_OBJECT_CLASS(purple_message_parent_class)->finalize(obj);
@@ -342,6 +361,36 @@
 		G_TYPE_ERROR,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleMessage:delivered:
+	 *
+	 * Whether or not the message was delivered. This is protocol dependent and
+	 * possibly client dependent as well. So if this is %FALSE that doesn't
+	 * necessarily mean the message was not delivered.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_DELIVERED] = g_param_spec_boolean(
+		"delivered", "delivered",
+		"Whether or not the message was delivered.",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleMessage:delivered-at:
+	 *
+	 * The time that the message was delivered. This is protocol dependent and
+	 * possibly client dependent as well. So if this is %NULL that doesn't
+	 * necessarily mean the message was not delivered.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_DELIVERED_AT] = g_param_spec_boxed(
+		"delivered-at", "delivered-at",
+		"The time that the message was delivered.",
+		G_TYPE_DATE_TIME,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -670,3 +719,80 @@
 
 	g_hash_table_remove_all(message->attachments);
 }
+
+gboolean
+purple_message_get_delivered(PurpleMessage *message) {
+	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
+
+	return message->delivered;
+}
+
+void
+purple_message_set_delivered(PurpleMessage *message, gboolean delivered) {
+	g_return_if_fail(PURPLE_IS_MESSAGE(message));
+
+	if(message->delivered != delivered) {
+		GObject *obj = G_OBJECT(message);
+
+		message->delivered = delivered;
+
+		g_clear_pointer(&message->delivered_at, g_date_time_unref);
+		if(message->delivered) {
+			message->delivered_at = g_date_time_new_now_utc();
+		}
+
+		g_object_freeze_notify(obj);
+		g_object_notify_by_pspec(obj, properties[PROP_DELIVERED]);
+		g_object_notify_by_pspec(obj, properties[PROP_DELIVERED_AT]);
+		g_object_thaw_notify(obj);
+	}
+}
+
+GDateTime *
+purple_message_get_delivered_at(PurpleMessage *message) {
+	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), NULL);
+
+	return message->delivered_at;
+}
+
+void
+purple_message_set_delivered_at(PurpleMessage *message, GDateTime *datetime) {
+	GObject *obj = NULL;
+
+	g_return_if_fail(PURPLE_IS_MESSAGE(message));
+
+	obj = G_OBJECT(message);
+
+	/* If there are any changes here, we need to manually change both delivered
+	 * and delivered at because we can't call purple_message_set_delivered as
+	 * it would need to call use if the caller called it and then we'd just
+	 * just have an infinite loop.
+	 */
+
+	if(datetime == NULL) {
+		if(message->delivered_at == NULL) {
+			return;
+		}
+
+		g_clear_pointer(&message->delivered_at, g_date_time_unref);
+		message->delivered = FALSE;
+	} else {
+		if(message->delivered_at != NULL) {
+			if(g_date_time_equal(message->delivered_at, datetime)) {
+				return;
+			}
+
+			g_clear_pointer(&message->delivered_at, g_date_time_unref);
+			message->delivered_at = g_date_time_ref(datetime);
+			message->delivered = TRUE;
+		} else {
+			message->delivered_at = g_date_time_ref(datetime);
+			message->delivered = TRUE;
+		}
+	}
+
+	g_object_freeze_notify(obj);
+	g_object_notify_by_pspec(obj, properties[PROP_DELIVERED]);
+	g_object_notify_by_pspec(obj, properties[PROP_DELIVERED_AT]);
+	g_object_thaw_notify(obj);
+}
diff --git a/libpurple/tests/test_message.c b/libpurple/tests/test_message.c
--- a/libpurple/tests/test_message.c
+++ b/libpurple/tests/test_message.c
@@ -21,6 +21,19 @@
 #include <purple.h>
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_message_notify_counter_cb(G_GNUC_UNUSED GObject *obj,
+                                      G_GNUC_UNUSED GParamSpec *pspec,
+                                      gpointer data)
+{
+	guint *counter = data;
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
  * Tests
  *****************************************************************************/
 static void
@@ -30,6 +43,7 @@
 	PurpleMessageFlags flags = 0;
 	GDateTime *timestamp = NULL;
 	GDateTime *timestamp1 = NULL;
+	GDateTime *delivered_at1 = NULL;
 	GError *error = NULL;
 	GError *error1 = NULL;
 	char *id = NULL;
@@ -38,17 +52,23 @@
 	char *author_name_color = NULL;
 	char *recipient = NULL;
 	char *contents = NULL;
+	gboolean delivered = FALSE;
 
 	timestamp = g_date_time_new_from_unix_utc(911347200);
 	error = g_error_new(g_quark_from_static_string("test-message"), 0,
 	                    "delivery failed");
 
+	/* We don't set delivered-at here because delivered will set it for us so
+	 * it's impossible to check that here, which is why we have separate tests
+	 * to do that check. We do verify that delivered-at was set though.
+	 */
 	message = g_object_new(
 		PURPLE_TYPE_MESSAGE,
 		"id", "id",
 		"author", "author",
 		"author-alias", "alias",
 		"author-name-color", "purple",
+		"delivered", TRUE,
 		"recipient", "pidgy",
 		"contents", "Now that is a big door",
 		"content-type", PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN,
@@ -63,6 +83,8 @@
 		"author", &author,
 		"author-alias", &author_alias,
 		"author-name-color", &author_name_color,
+		"delivered", &delivered,
+		"delivered-at", &delivered_at1,
 		"recipient", &recipient,
 		"contents", &contents,
 		"content-type", &content_type,
@@ -75,6 +97,8 @@
 	g_assert_cmpstr(author, ==, "author");
 	g_assert_cmpstr(author_alias, ==, "alias");
 	g_assert_cmpstr(author_name_color, ==, "purple");
+	g_assert_true(delivered);
+	g_assert_nonnull(delivered_at1);
 	g_assert_cmpstr(recipient, ==, "pidgy");
 	g_assert_cmpstr(contents, ==, "Now that is a big door");
 	g_assert_cmpint(content_type, ==, PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN);
@@ -86,6 +110,7 @@
 	g_clear_pointer(&author, g_free);
 	g_clear_pointer(&author_alias, g_free);
 	g_clear_pointer(&author_name_color, g_free);
+	g_clear_pointer(&delivered_at1, g_date_time_unref);
 	g_clear_pointer(&recipient, g_free);
 	g_clear_pointer(&contents, g_free);
 	g_clear_pointer(&timestamp, g_date_time_unref);
@@ -94,6 +119,83 @@
 	g_clear_object(&message);
 }
 
+static void
+test_purple_message_delivered_set_delivered_at(void) {
+	PurpleMessage *message = NULL;
+	guint delivered_counter = 0;
+	guint delivered_at_counter = 0;
+
+	message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+	g_signal_connect(message, "notify::delivered",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &delivered_counter);
+	g_signal_connect(message, "notify::delivered-at",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &delivered_at_counter);
+
+	/* The default delivery state is FALSE, so setting it to true, should call
+	 * the notify signals and set delivered-at.
+	 */
+	purple_message_set_delivered(message, TRUE);
+	g_assert_true(purple_message_get_delivered(message));
+	g_assert_nonnull(purple_message_get_delivered_at(message));
+	g_assert_cmpuint(delivered_counter, ==, 1);
+	g_assert_cmpuint(delivered_at_counter, ==, 1);
+
+	/* Now clear everything and verify it's empty. */
+	delivered_counter = 0;
+	delivered_at_counter = 0;
+
+	purple_message_set_delivered(message, FALSE);
+	g_assert_false(purple_message_get_delivered(message));
+	g_assert_null(purple_message_get_delivered_at(message));
+	g_assert_cmpuint(delivered_counter, ==, 1);
+	g_assert_cmpuint(delivered_at_counter, ==, 1);
+
+	g_clear_object(&message);
+}
+
+static void
+test_purple_message_delivered_at_set_delivered(void) {
+	PurpleMessage *message = NULL;
+	GDateTime *delivered_at = NULL;
+	GDateTime *delivered_at1 = NULL;
+	guint delivered_counter = 0;
+	guint delivered_at_counter = 0;
+
+	message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+	g_signal_connect(message, "notify::delivered",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &delivered_counter);
+	g_signal_connect(message, "notify::delivered-at",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &delivered_at_counter);
+
+	/* The default value for delivered-at is NULL, so setting it to non-null
+	 * should emit the signals and everything.
+	 */
+	delivered_at = g_date_time_new_now_utc();
+	purple_message_set_delivered_at(message, delivered_at);
+	g_assert_true(purple_message_get_delivered(message));
+	delivered_at1 = purple_message_get_delivered_at(message);
+	g_assert_nonnull(delivered_at1);
+	g_assert_true(g_date_time_equal(delivered_at1, delivered_at));
+	g_assert_cmpuint(delivered_counter, ==, 1);
+	g_assert_cmpuint(delivered_at_counter, ==, 1);
+
+	/* Now clear everything and make sure it's all good. */
+	delivered_counter = 0;
+	delivered_at_counter = 0;
+	purple_message_set_delivered_at(message, NULL);
+	g_assert_false(purple_message_get_delivered(message));
+	g_assert_null(purple_message_get_delivered_at(message));
+	g_assert_cmpuint(delivered_counter, ==, 1);
+	g_assert_cmpuint(delivered_at_counter, ==, 1);
+
+	g_clear_pointer(&delivered_at, g_date_time_unref);
+	g_clear_object(&message);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -104,5 +206,10 @@
 	g_test_add_func("/message/properties",
 	                test_purple_message_properties);
 
+	g_test_add_func("/message/delivered-sets-delivered-at",
+	                test_purple_message_delivered_set_delivered_at);
+	g_test_add_func("/message/delivered-at-sets-delivered",
+	                test_purple_message_delivered_at_set_delivered);
+
 	return g_test_run();
 }
