diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index 43ea3ae54f875af959f546edebcc59c989620684..5d4e595a93cb3f8de23d589a1dbe6bdd720bdd51 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -171,6 +171,9 @@ public interface DatabaseComponent {
 	/** Records the user's rating for the given author. */
 	void setRating(AuthorId a, Rating r) throws DbException;
 
+	/** Records the given messages as having been seen by the given contact. */
+	void setSeen(ContactId c, Collection<MessageId> seen) throws DbException;
+
 	/**
 	 * Sets the configuration for the transport with the given name, replacing
 	 * any existing configuration for that transport.
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 97e3a66fab24fd192cffd2810409f444f474367b..c43c778a2ace0c9f9cf227d51c00d9201ae5ff7f 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -963,6 +963,8 @@ DatabaseCleaner.Callback {
 
 	public void receiveOffer(ContactId c, Offer o, RequestWriter r)
 	throws DbException, IOException {
+		Collection<MessageId> offered;
+		BitSet request;
 		contactLock.readLock().lock();
 		try {
 			if(!containsContact(c)) throw new NoSuchContactException();
@@ -972,10 +974,10 @@ DatabaseCleaner.Callback {
 				try {
 					subscriptionLock.readLock().lock();
 					try {
-						Collection<MessageId> offered = o.getMessageIds();
-						BitSet request = new BitSet(offered.size());
 						T txn = db.startTransaction();
 						try {
+							offered = o.getMessageIds();
+							request = new BitSet(offered.size());
 							Iterator<MessageId> it = offered.iterator();
 							for(int i = 0; it.hasNext(); i++) {
 								// If the message is not in the database, or if
@@ -989,7 +991,6 @@ DatabaseCleaner.Callback {
 							db.abortTransaction(txn);
 							throw e;
 						}
-						r.writeRequest(request, offered.size());
 					} finally {
 						subscriptionLock.readLock().unlock();
 					}
@@ -1002,6 +1003,7 @@ DatabaseCleaner.Callback {
 		} finally {
 			contactLock.readLock().unlock();
 		}
+		r.writeRequest(request, offered.size());
 	}
 
 	public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate s)
@@ -1147,6 +1149,41 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public void setSeen(ContactId c, Collection<MessageId> seen)
+	throws DbException {
+		contactLock.readLock().lock();
+		try {
+			if(!containsContact(c)) throw new NoSuchContactException();
+			messageLock.readLock().lock();
+			try {
+				messageStatusLock.writeLock().lock();
+				try {
+					subscriptionLock.readLock().lock();
+					try {
+						T txn = db.startTransaction();
+						try {
+							for(MessageId m : seen) {
+								db.setStatus(txn, c, m, Status.SEEN);
+							}
+							db.commitTransaction(txn);
+						} catch(DbException e) {
+							db.abortTransaction(txn);
+							throw e;
+						}
+					} finally {
+						subscriptionLock.readLock().unlock();
+					}
+				} finally {
+					messageStatusLock.writeLock().unlock();
+				}
+			} finally {
+				messageLock.readLock().unlock();
+			}
+		} finally {
+			contactLock.readLock().unlock();
+		}
+	}
+
 	/**
 	 * Updates the sendability of all messages written by the given author, and
 	 * the ancestors of those messages if necessary.
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
index 0a0bdd5a06cebfcaf9769ed357b05b98014a9b8e..4ebfe4c726b79b9b1d20fc98c880906eb2b9a332 100644
--- a/test/net/sf/briar/db/DatabaseComponentTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -586,11 +586,11 @@ public abstract class DatabaseComponentTest extends TestCase {
 		final TransportUpdate transportsUpdate = context.mock(TransportUpdate.class);
 		context.checking(new Expectations() {{
 			// Check whether the contact is still in the DB (which it's not)
-			exactly(17).of(database).startTransaction();
+			exactly(18).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(17).of(database).containsContact(txn, contactId);
+			exactly(18).of(database).containsContact(txn, contactId);
 			will(returnValue(false));
-			exactly(17).of(database).commitTransaction(txn);
+			exactly(18).of(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner);
 
@@ -680,6 +680,11 @@ public abstract class DatabaseComponentTest extends TestCase {
 			fail();
 		} catch(NoSuchContactException expected) {}
 
+		try {
+			db.setSeen(contactId, Collections.singleton(messageId));
+			fail();
+		} catch(NoSuchContactException expected) {}
+
 		context.assertIsSatisfied();
 	}
 
@@ -1488,4 +1493,26 @@ public abstract class DatabaseComponentTest extends TestCase {
 
 		context.assertIsSatisfied();
 	}
+
+	@Test
+	public void testSetSeen() throws Exception {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		context.checking(new Expectations() {{
+			allowing(database).startTransaction();
+			will(returnValue(txn));
+			allowing(database).commitTransaction(txn);
+			allowing(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			// setSeen(contactId, Collections.singleton(messageId))
+			oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN);
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner);
+
+		db.setSeen(contactId, Collections.singleton(messageId));
+
+		context.assertIsSatisfied();
+	}
 }