diff --git a/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift b/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
index 65bceac997e7dbb7190d08376a1890ed54f7c4a8..9427053788da3f869e9dd6b0845d581c7e8905c2 100644
--- a/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
+++ b/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
@@ -27,12 +27,52 @@ extension SendGroupMessage {
     now: @escaping () -> Date
   ) -> SendGroupMessage {
     SendGroupMessage { text, groupId, onError, completion in
-      // TODO: implement sending group message
-      struct Unimplemented: Error, LocalizedError {
-        var errorDescription: String? { "SendGroupMessage is not implemented!" }
+      do {
+        let chat = try messenger.groupChat.tryGet()
+        let myContactId = try messenger.e2e.tryGet().getContact().getId()
+        var message = try db().saveMessage(.init(
+          senderId: myContactId,
+          recipientId: nil,
+          groupId: groupId,
+          date: now(),
+          status: .sending,
+          isUnread: false,
+          text: text
+        ))
+        let payload = MessagePayload(text: message.text)
+        let report = try chat.send(
+          groupId: groupId,
+          message: try payload.encode()
+        )
+        message.networkId = report.messageId
+        message.roundURL = report.roundURL
+        message = try db().saveMessage(message)
+        try messenger.cMix.tryGet().waitForRoundResult(
+          roundList: try report.encode(),
+          timeoutMS: 30_000,
+          callback: .init { result in
+            let status: XXModels.Message.Status
+            switch result {
+            case .delivered(_):
+              status = .sent
+            case .notDelivered(let timedOut):
+              status = timedOut ? .sendingTimedOut : .sendingFailed
+            }
+            do {
+              try db().bulkUpdateMessages(
+                .init(id: [message.id]),
+                .init(status: status)
+              )
+            } catch {
+              onError(error)
+            }
+            completion()
+          }
+        )
+      } catch {
+        onError(error)
+        completion()
       }
-      onError(Unimplemented())
-      completion()
     }
   }
 }
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
index a53e5e35e49a1fda9f12a0a11b751951da4a2df5..1df2eaa3717344509c590a462036218c0ff416b0 100644
--- a/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
+++ b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
@@ -10,6 +10,13 @@ final class SendGroupMessageTests: XCTestCase {
   enum Action: Equatable {
     case didReceiveError(String)
     case didComplete
+    case didSaveMessage(XXModels.Message)
+    case didSend(groupId: Data, message: Data, tag: String?)
+    case didWaitForRoundResults(roundList: Data, timeoutMS: Int)
+    case didUpdateMessage(
+      query: XXModels.Message.Query,
+      assignments: XXModels.Message.Assignments
+    )
   }
 
   var actions: [Action]!
@@ -25,9 +32,58 @@ final class SendGroupMessageTests: XCTestCase {
   func testSend() {
     let text = "Hello!"
     let groupId = "group-id".data(using: .utf8)!
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let messageId: Int64 = 321
+    let sendReport = GroupSendReport(
+      rounds: [],
+      roundURL: "round-url",
+      timestamp: 1234,
+      messageId: "message-id".data(using: .utf8)!
+    )
+
+    var messageDeliveryCallback: MessageDeliveryCallback?
 
     var messenger: Messenger = .unimplemented
+    messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.send.run = { groupId, message, tag in
+        self.actions.append(.didSend(groupId: groupId, message: message, tag: tag))
+        return sendReport
+      }
+      return groupChat
+    }
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in myContactId }
+        return contact
+      }
+      return e2e
+    }
+    messenger.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForRoundResult.run = { roundList, timeoutMS, callback in
+        self.actions.append(.didWaitForRoundResults(roundList: roundList, timeoutMS: timeoutMS))
+        messageDeliveryCallback = callback
+      }
+      return cMix
+    }
     var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveMessage.run = { message in
+        self.actions.append(.didSaveMessage(message))
+        var message = message
+        message.id = messageId
+        return message
+      }
+      db.bulkUpdateMessages.run = { query, assignments in
+        self.actions.append(.didUpdateMessage(query: query, assignments: assignments))
+        return 1
+      }
+      return db
+    }
     let now = Date()
     let send: SendGroupMessage = .live(
       messenger: messenger,
@@ -47,9 +103,177 @@ final class SendGroupMessageTests: XCTestCase {
     )
 
     XCTAssertNoDifference(actions, [
-      .didReceiveError("SendGroupMessage is not implemented!"),
+      .didSaveMessage(.init(
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: now,
+        status: .sending,
+        isUnread: false,
+        text: text
+      )),
+      .didSend(
+        groupId: groupId,
+        message: try! MessagePayload(text: text).encode(),
+        tag: nil
+      ),
+      .didSaveMessage(.init(
+        id: messageId,
+        networkId: sendReport.messageId,
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: now,
+        status: .sending,
+        isUnread: false,
+        text: text,
+        roundURL: sendReport.roundURL
+      )),
+      .didWaitForRoundResults(
+        roundList: try! sendReport.encode(),
+        timeoutMS: 30_000
+      ),
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.delivered(roundResults: []))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sent)
+      ),
+      .didComplete,
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.notDelivered(timedOut: true))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sendingTimedOut)
+      ),
+      .didComplete,
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.notDelivered(timedOut: false))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sendingFailed)
+      ),
+      .didComplete,
+    ])
+  }
+
+  func testSendDatabaseFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var messenger: Messenger = .unimplemented
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in Data() }
+        return contact
+      }
+      return e2e
+    }
+    messenger.groupChat.get = { .unimplemented }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = { throw failure }
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: XCTestDynamicOverlay.unimplemented("now", placeholder: Date())
+    )
+
+    send(
+      text: "Hello",
+      to: "group-id".data(using: .utf8)!,
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didReceiveError(failure.localizedDescription),
       .didComplete
     ])
   }
-}
 
+  func testBulkUpdateOnDeliveryFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var messageDeliveryCallback: MessageDeliveryCallback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.send.run = { _, _, _ in
+        GroupSendReport(
+          rounds: [],
+          roundURL: "",
+          timestamp: 0,
+          messageId: Data()
+        )
+      }
+      return groupChat
+    }
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in Data() }
+        return contact
+      }
+      return e2e
+    }
+    messenger.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForRoundResult.run = { _, _, callback in
+        messageDeliveryCallback = callback
+      }
+      return cMix
+    }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveMessage.run = { message in message }
+      db.bulkUpdateMessages.run = { _, _ in throw failure }
+      return db
+    }
+    let now = Date()
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: { now }
+    )
+
+    send(
+      text: "Hello",
+      to: Data(),
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    messageDeliveryCallback?.handle(.delivered(roundResults: []))
+
+    XCTAssertNoDifference(actions, [
+      .didReceiveError(failure.localizedDescription),
+      .didComplete,
+    ])
+  }
+}