diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 08b420d59c4728ec7e827b074c4acd306f8eca3b..c7a9a51c453342f8fee536f46ee22aee868c496d 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -256,15 +256,6 @@ interface Database<T> {
 	 */
 	Collection<Transport> getLocalTransports(T txn) throws DbException;
 
-	/**
-	 * Returns the IDs of any messages sent to the given contact that should
-	 * now be considered lost.
-	 * <p>
-	 * Locking: contact read, message read, messageStatus read.
-	 */
-	Collection<MessageId> getLostMessages(T txn, ContactId c)
-			throws DbException;
-
 	/**
 	 * Returns the message identified by the given ID, in serialised form.
 	 * <p>
@@ -307,23 +298,14 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the IDs of any messages received from the given contact that
-	 * need to be acknowledged.
+	 * Returns the IDs of some messages received from the given contact that
+	 * need to be acknowledged, up to the given number of messages.
 	 * <p>
 	 * Locking: contact read, messageStatus read.
 	 */
 	Collection<MessageId> getMessagesToAck(T txn, ContactId c, int maxMessages)
 			throws DbException;
 
-	/**
-	 * Returns the number of children of the message identified by the given
-	 * ID that are present in the database and have sendability scores greater
-	 * than zero.
-	 * <p>
-	 * Locking: message read.
-	 */
-	int getNumberOfSendableChildren(T txn, MessageId m) throws DbException;
-
 	/**
 	 * Returns the IDs of some messages that are eligible to be sent to the
 	 * given contact, up to the given number of messages.
@@ -331,9 +313,18 @@ interface Database<T> {
 	 * Locking: contact read, message read, messageStatus read,
 	 * subscription read.
 	 */
-	Collection<MessageId> getOfferableMessages(T txn, ContactId c,
+	Collection<MessageId> getMessagesToOffer(T txn, ContactId c,
 			int maxMessages) throws DbException;
 
+	/**
+	 * Returns the number of children of the message identified by the given
+	 * ID that are present in the database and have sendability scores greater
+	 * than zero.
+	 * <p>
+	 * Locking: message read.
+	 */
+	int getNumberOfSendableChildren(T txn, MessageId m) throws DbException;
+
 	/**
 	 * Returns the IDs of the oldest messages in the database, with a total
 	 * size less than or equal to the given size.
@@ -354,7 +345,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: message read, messageFlag read.
 	 */
-	boolean getRead(T txn, MessageId m) throws DbException;
+	boolean getReadFlag(T txn, MessageId m) throws DbException;
 
 	/**
 	 * Returns all remote properties for the given transport.
@@ -394,7 +385,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: message read, messageFlag read.
 	 */
-	boolean getStarred(T txn, MessageId m) throws DbException;
+	boolean getStarredFlag(T txn, MessageId m) throws DbException;
 
 	/**
 	 * Returns the groups to which the user subscribes.
@@ -500,8 +491,8 @@ interface Database<T> {
 	 * <p>
 	 * Locking: contact read, message read, messageStatus write.
 	 */
-	void removeAckedMessages(T txn, ContactId c, Collection<MessageId> acked)
-			throws DbException;
+	void removeOutstandingMessages(T txn, ContactId c,
+			Collection<MessageId> acked) throws DbException;
 
 	/**
 	 * Marks the given messages received from the given contact as having been
@@ -520,16 +511,6 @@ interface Database<T> {
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
-	/**
-	 * Removes outstanding messages that have been lost. Any messages that are
-	 * still considered outstanding (Status.SENT) with respect to the given
-	 * contact are now considered unsent (Status.NEW).
-	 * <p>
-	 * Locking: contact read, message read, messageStatus write.
-	 */
-	void removeLostMessages(T txn, ContactId c, Collection<MessageId> lost)
-			throws DbException;
-
 	/**
 	 * Removes a message (and all associated state) from the database.
 	 * <p>
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index 2fa615d6018353a287bbb0c700b49ca696af9028..d9333bcd2bf4e0fb31d204539ba61f87530acc5a 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -612,7 +612,7 @@ DatabaseCleaner.Callback {
 					try {
 						if(!db.containsContact(txn, c))
 							throw new NoSuchContactException();
-						offered = db.getOfferableMessages(txn, c, maxMessages);
+						offered = db.getMessagesToOffer(txn, c, maxMessages);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
@@ -1048,12 +1048,7 @@ DatabaseCleaner.Callback {
 					try {
 						if(!db.containsContact(txn, c))
 							throw new NoSuchContactException();
-						// Mark all acked messages as seen
-						db.removeAckedMessages(txn, c, a.getMessageIds());
-						// Find any lost messages that need to be retransmitted
-						// FIXME: Merge these methods
-						Collection<MessageId> lost = db.getLostMessages(txn, c);
-						if(!lost.isEmpty()) db.removeLostMessages(txn, c, lost);
+						db.removeOutstandingMessages(txn, c, a.getMessageIds());
 						db.commitTransaction(txn);
 					} catch(DbException e) {
 						db.abortTransaction(txn);
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index 10e29501bdac7c6088b84964491b18ae821fc84e..81c1f63375e3cfca9540fcd38f749792a3359ed8 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -64,9 +64,9 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private static final String CREATE_MESSAGES =
 			"CREATE TABLE messages"
 					+ " (messageId HASH NOT NULL,"
-					+ " parentId HASH," // Null for the first message in a thread
+					+ " parentId HASH," // Null for the first msg in a thread
 					+ " groupId HASH," // Null for private messages
-					+ " authorId HASH," // Null for private or anonymous messages
+					+ " authorId HASH," // Null for private or anonymous msgs
 					+ " subject VARCHAR NOT NULL,"
 					+ " timestamp BIGINT NOT NULL,"
 					+ " length INT NOT NULL,"
@@ -76,7 +76,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " sendability INT," // Null for private messages
 					+ " contactId INT," // Null for group messages
 					+ " PRIMARY KEY (messageId),"
-					+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
+					+ " FOREIGN KEY (groupId)"
+					+ " REFERENCES subscriptions (groupId)"
 					+ " ON DELETE CASCADE,"
 					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE)";
@@ -101,7 +102,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " deleted BIGINT NOT NULL,"
 					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE,"
-					+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
+					+ " FOREIGN KEY (groupId)"
+					+ " REFERENCES subscriptions (groupId)"
 					+ " ON DELETE CASCADE)";
 
 	private static final String INDEX_VISIBILITIES_BY_GROUP =
@@ -537,16 +539,29 @@ abstract class JdbcDatabase implements Database<Connection> {
 	public void addMessageToAck(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
+		ResultSet rs = null;
 		try {
-			String sql = "INSERT INTO messagesToAck (messageId, contactId)"
+			String sql = "SELECT NULL FROM messagesToAck"
+					+ " WHERE messageId = ? AND contactId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			ps.setInt(2, c.getInt());
+			rs = ps.executeQuery();
+			boolean found = rs.next();
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			if(found) return;
+			sql = "INSERT INTO messagesToAck (messageId, contactId)"
 					+ " VALUES (?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
 			int affected = ps.executeUpdate();
-			if(affected > 1) throw new DbStateException();
+			if(affected != 1) throw new DbStateException();
 			ps.close();
 		} catch(SQLException e) {
+			tryToClose(rs);
 			tryToClose(ps);
 			throw new DbException(e);
 		}
@@ -1117,12 +1132,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<MessageId> getLostMessages(Connection txn, ContactId c)
-			throws DbException {
-		// FIXME: Retransmission
-		return Collections.emptyList();
-	}
-
 	public byte[] getMessage(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -1319,6 +1328,64 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Collection<MessageId> getMessagesToOffer(Connection txn,
+			ContactId c, int maxMessages) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			// Do we have any sendable private messages?
+			String sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " WHERE m.contactId = ? AND status = ?"
+					+ " ORDER BY timestamp"
+					+ " LIMIT ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setShort(2, (short) Status.NEW.ordinal());
+			ps.setInt(3, maxMessages);
+			rs = ps.executeQuery();
+			List<MessageId> ids = new ArrayList<MessageId>();
+			while(rs.next()) ids.add(new MessageId(rs.getBytes(2)));
+			rs.close();
+			ps.close();
+			if(ids.size() == maxMessages)
+				return Collections.unmodifiableList(ids);
+			// Do we have any sendable group messages?
+			sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN contactSubscriptions AS cs"
+					+ " ON m.groupId = cs.groupId"
+					+ " JOIN visibilities AS v"
+					+ " ON m.groupId = v.groupId"
+					+ " AND cs.contactId = v.contactId"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " AND cs.contactId = s.contactId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON cs.contactId = st.contactId"
+					+ " WHERE cs.contactId = ?"
+					+ " AND timestamp >= start"
+					+ " AND timestamp >= expiry"
+					+ " AND status = ?"
+					+ " AND sendability > ZERO()"
+					+ " ORDER BY timestamp"
+					+ " LIMIT ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setShort(2, (short) Status.NEW.ordinal());
+			ps.setInt(3, maxMessages - ids.size());
+			rs = ps.executeQuery();
+			while(rs.next()) ids.add(new MessageId(rs.getBytes(2)));
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableList(ids);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public int getNumberOfSendableChildren(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1403,7 +1470,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean getRead(Connection txn, MessageId m) throws DbException {
+	public boolean getReadFlag(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1519,64 +1586,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<MessageId> getOfferableMessages(Connection txn,
-			ContactId c, int maxMessages) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			// Do we have any sendable private messages?
-			String sql = "SELECT m.messageId FROM messages AS m"
-					+ " JOIN statuses AS s"
-					+ " ON m.messageId = s.messageId"
-					+ " WHERE m.contactId = ? AND status = ?"
-					+ " ORDER BY timestamp"
-					+ " LIMIT ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setShort(2, (short) Status.NEW.ordinal());
-			ps.setInt(3, maxMessages);
-			rs = ps.executeQuery();
-			List<MessageId> ids = new ArrayList<MessageId>();
-			while(rs.next()) ids.add(new MessageId(rs.getBytes(2)));
-			rs.close();
-			ps.close();
-			if(ids.size() == maxMessages)
-				return Collections.unmodifiableList(ids);
-			// Do we have any sendable group messages?
-			sql = "SELECT m.messageId FROM messages AS m"
-					+ " JOIN contactSubscriptions AS cs"
-					+ " ON m.groupId = cs.groupId"
-					+ " JOIN visibilities AS v"
-					+ " ON m.groupId = v.groupId"
-					+ " AND cs.contactId = v.contactId"
-					+ " JOIN statuses AS s"
-					+ " ON m.messageId = s.messageId"
-					+ " AND cs.contactId = s.contactId"
-					+ " JOIN subscriptionTimes AS st"
-					+ " ON cs.contactId = st.contactId"
-					+ " WHERE cs.contactId = ?"
-					+ " AND timestamp >= start"
-					+ " AND timestamp >= expiry"
-					+ " AND status = ?"
-					+ " AND sendability > ZERO()"
-					+ " ORDER BY timestamp"
-					+ " LIMIT ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setShort(2, (short) Status.NEW.ordinal());
-			ps.setInt(3, maxMessages - ids.size());
-			rs = ps.executeQuery();
-			while(rs.next()) ids.add(new MessageId(rs.getBytes(2)));
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(ids);
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Collection<MessageId> getSendableMessages(Connection txn,
 			ContactId c, int maxLength) throws DbException {
 		PreparedStatement ps = null;
@@ -1641,7 +1650,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean getStarred(Connection txn, MessageId m) throws DbException {
+	public boolean getStarredFlag(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1975,28 +1984,24 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void removeAckedMessages(Connection txn, ContactId c,
+	public void removeOutstandingMessages(Connection txn, ContactId c,
 			Collection<MessageId> acked) throws DbException {
-		setStatus(txn, c, acked, Status.SEEN);
-	}
-
-	private void setStatus(Connection txn, ContactId c,
-			Collection<MessageId> ids, Status newStatus) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			// Set the status of each message if it's currently SENT
+			// Set the status of each message to SEEN if it's currently SENT
 			String sql = "UPDATE statuses SET status = ?"
 					+ " WHERE messageId = ? AND contactId = ? AND status = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setShort(1, (short) newStatus.ordinal());
+			ps.setShort(1, (short) Status.SEEN.ordinal());
 			ps.setInt(3, c.getInt());
 			ps.setShort(4, (short) Status.SENT.ordinal());
-			for(MessageId m : ids) {
+			for(MessageId m : acked) {
 				ps.setBytes(2, m.getBytes());
 				ps.addBatch();
 			}
 			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != ids.size()) throw new DbStateException();
+			if(batchAffected.length != acked.size())
+				throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] > 1) throw new DbStateException();
 			}
@@ -2048,11 +2053,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void removeLostMessages(Connection txn, ContactId c,
-			Collection<MessageId> lost) throws DbException {
-		setStatus(txn, c, lost, Status.NEW);
-	}
-
 	public void removeMessage(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index 5a2fabba23ffefc56ed4d11373625b3d5fb4c243..4e03a034116232fe268608712e3e423cb2a7011b 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -755,7 +755,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			allowing(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// Get the sendable message IDs
-			oneOf(database).getOfferableMessages(txn, contactId, 123);
+			oneOf(database).getMessagesToOffer(txn, contactId, 123);
 			will(returnValue(offerable));
 			// Create the packet
 			oneOf(packetFactory).createOffer(offerable);
@@ -898,11 +898,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			// Get the acked messages
 			oneOf(ack).getMessageIds();
 			will(returnValue(Collections.singletonList(messageId)));
-			oneOf(database).removeAckedMessages(txn, contactId,
+			oneOf(database).removeOutstandingMessages(txn, contactId,
 					Collections.singletonList(messageId));
-			// Find lost messages
-			oneOf(database).getLostMessages(txn, contactId);
-			will(returnValue(Collections.emptyList()));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index e9ba2d0b74c627e950b84496e31a765b147054dd..8e9a2c23b074abd072c0c200894d15785086971e 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -556,7 +556,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testRemoveAckedMessage() throws Exception {
+	public void testOutstandingMessageAcked() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
@@ -584,7 +584,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertFalse(it.hasNext());
 
 		// Pretend that the message was acked
-		db.removeAckedMessages(txn, contactId,
+		db.removeOutstandingMessages(txn, contactId,
 				Collections.singletonList(messageId));
 
 		// The message still should not be sendable
@@ -595,48 +595,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testRemoveLostMessage() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn));
-		db.addSubscription(txn, group);
-		db.addVisibility(txn, contactId, groupId);
-		db.addSubscription(txn, contactId, group, 0L);
-		db.addGroupMessage(txn, message);
-		db.setSendability(txn, messageId, 1);
-		db.setStatus(txn, contactId, messageId, Status.NEW);
-
-		// Get the message and mark it as sent
-		Iterator<MessageId> it =
-				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
-		db.setStatus(txn, contactId, messageId, Status.SENT);
-		db.addOutstandingMessages(txn, contactId,
-				Collections.singletonList(messageId));
-
-		// The message should no longer be sendable
-		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
-		assertFalse(it.hasNext());
-
-		// Pretend that the message was lost
-		db.removeLostMessages(txn, contactId,
-				Collections.singletonList(messageId));
-
-		// The message should be sendable again
-		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testGetMessagesByAuthor() throws Exception {
 		AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
@@ -1467,12 +1425,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroupMessage(txn, message);
 
 		// The message should be unread by default
-		assertFalse(db.getRead(txn, messageId));
+		assertFalse(db.getReadFlag(txn, messageId));
 		// Marking the message read should return the old value
 		assertFalse(db.setRead(txn, messageId, true));
 		assertTrue(db.setRead(txn, messageId, true));
 		// The message should be read
-		assertTrue(db.getRead(txn, messageId));
+		assertTrue(db.getReadFlag(txn, messageId));
 		// Marking the message unread should return the old value
 		assertTrue(db.setRead(txn, messageId, false));
 		assertFalse(db.setRead(txn, messageId, false));
@@ -1493,12 +1451,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroupMessage(txn, message);
 
 		// The message should be unstarred by default
-		assertFalse(db.getStarred(txn, messageId));
+		assertFalse(db.getStarredFlag(txn, messageId));
 		// Starring the message should return the old value
 		assertFalse(db.setStarred(txn, messageId, true));
 		assertTrue(db.setStarred(txn, messageId, true));
 		// The message should be starred
-		assertTrue(db.getStarred(txn, messageId));
+		assertTrue(db.getStarredFlag(txn, messageId));
 		// Unstarring the message should return the old value
 		assertTrue(db.setStarred(txn, messageId, false));
 		assertFalse(db.setStarred(txn, messageId, false));