diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index 2f5c3321f0d2a8af8abe3db1410a46e67209a896..4d50c4cc5c656ff93c1b684e53306575e1e4c969 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -144,6 +144,9 @@ public interface DatabaseComponent {
 	/** Returns the set of groups to which the user subscribes. */
 	Collection<Group> getSubscriptions() throws DbException;
 
+	/** Returns the number of unread messages in each subscribed group. */
+	Map<GroupId, Integer> getUnreadMessageCounts() throws DbException;
+
 	/** Returns the contacts to which the given group is visible. */
 	Collection<ContactId> getVisibility(GroupId g) throws DbException;
 
diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java
index b65f99dbee593d7d27744541e59f620d0fb1e79a..e8c9d05bd8e5874e260a6c40721dc4c5c5f8ddf3 100644
--- a/components/net/sf/briar/db/Database.java
+++ b/components/net/sf/briar/db/Database.java
@@ -406,6 +406,13 @@ interface Database<T> {
 	 */
 	long getTransportsSent(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Returns the number of unread messages in each subscribed group.
+	 * <p>
+	 * Locking: message read, messageFlag read, subscription read.
+	 */
+	Map<GroupId, Integer> getUnreadMessageCounts(T txn) throws DbException;
+
 	/**
 	 * Returns the contacts to which the given group is visible.
 	 * <p>
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 881a2c4bccdc7ade16e3227c263e324fdeba82ef..2457bd98bbd6051b72ca447b3cf46d51a0f8c256 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -885,6 +885,34 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public Map<GroupId, Integer> getUnreadMessageCounts() throws DbException {
+		messageLock.readLock().lock();
+		try {
+			messageFlagLock.readLock().lock();
+			try {
+				subscriptionLock.readLock().lock();
+				try {
+					T txn = db.startTransaction();
+					try {
+						Map<GroupId, Integer> counts =
+							db.getUnreadMessageCounts(txn);
+						db.commitTransaction(txn);
+						return counts;
+					} catch(DbException e) {
+						db.abortTransaction(txn);
+						throw e;
+					}
+				} finally {
+					subscriptionLock.readLock().unlock();
+				}
+			} finally {
+				messageFlagLock.readLock().unlock();
+			}
+		} finally {
+			messageLock.readLock().unlock();
+		}
+	}
+
 	public Collection<ContactId> getVisibility(GroupId g) throws DbException {
 		contactLock.readLock().lock();
 		try {
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index fe8e757bfc4cc04767b0514e2013d89fcb368230..1a4b57ca3e54889ee5c3f7a9b2b903cab1273665 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -1709,6 +1709,33 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Map<GroupId, Integer> getUnreadMessageCounts(Connection txn)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT groupId, COUNT(*)"
+				+ " FROM messages LEFT OUTER JOIN flags"
+				+ " ON messages.messageId = flags.messageId"
+				+ " WHERE (NOT read) OR (read IS NULL)"
+				+ " GROUP BY groupId";
+			ps = txn.prepareStatement(sql);
+			rs = ps.executeQuery();
+			Map<GroupId, Integer> counts = new HashMap<GroupId, Integer>();
+			while(rs.next()) {
+				GroupId g = new GroupId(rs.getBytes(1));
+				counts.put(g, rs.getInt(2));
+			}
+			rs.close();
+			ps.close();
+			return counts;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<ContactId> getVisibility(Connection txn, GroupId g)
 	throws DbException {
 		PreparedStatement ps = null;
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index 270085085ccfaad507e8396fa1cecb1bf899f149..e2ac13f2c9bc5fefb59373e926726368d50ded24 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -1776,6 +1776,62 @@ public class H2DatabaseTest extends TestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGetUnreadMessageCounts() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Subscribe to a couple of groups
+		db.addSubscription(txn, group);
+		GroupId groupId1 = new GroupId(TestUtils.getRandomId());
+		Group group1 = groupFactory.createGroup(groupId1, "Another group",
+				null);
+		db.addSubscription(txn, group1);
+
+		// Store two messages in the first group
+		db.addGroupMessage(txn, message);
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new TestMessage(messageId1, null, groupId,
+				authorId, subject, timestamp, raw);
+		db.addGroupMessage(txn, message1);
+
+		// Store one message in the second group
+		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
+		Message message2 = new TestMessage(messageId2, null, groupId1,
+				authorId, subject, timestamp, raw);
+		db.addGroupMessage(txn, message2);
+
+		// Mark one of the messages in the first group read
+		assertFalse(db.setRead(txn, messageId, true));
+
+		// There should be one unread message in each group
+		Map<GroupId, Integer> counts = db.getUnreadMessageCounts(txn);
+		assertEquals(2, counts.size());
+		Integer count = counts.get(groupId);
+		assertNotNull(count);
+		assertEquals(1, count.intValue());
+		count = counts.get(groupId1);
+		assertNotNull(count);
+		assertEquals(1, count.intValue());
+
+		// Mark the read message unread (it will now be false rather than null)
+		assertTrue(db.setRead(txn, messageId, false));
+
+		// Mark the message in the second group read
+		assertFalse(db.setRead(txn, messageId2, true));
+
+		// There should be two unread messages in the first group, none in
+		// the second group
+		counts = db.getUnreadMessageCounts(txn);
+		assertEquals(1, counts.size());
+		count = counts.get(groupId);
+		assertNotNull(count);
+		assertEquals(2, count.intValue());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testExceptionHandling() throws Exception {
 		Database<Connection> db = open(false);