diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index 38a7bf68daa2eed9b3b50ab19c6c49456c5c6b08..90caecf8c3f2b8d720a0f03eb44dffd46a494e3e 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -14,10 +14,8 @@ import net.sf.briar.api.protocol.Message;
 public interface DatabaseComponent {
 
 	static final long MEGABYTES = 1024L * 1024L;
-	static final long GIGABYTES = 1024L * MEGABYTES;
 
 	// FIXME: Some of these should be configurable
-	static final long MAX_DB_SIZE = 2L * GIGABYTES;
 	static final long MIN_FREE_SPACE = 300L * MEGABYTES;
 	static final long CRITICAL_FREE_SPACE = 100L * MEGABYTES;
 	static final long MAX_BYTES_BETWEEN_SPACE_CHECKS = 5L * MEGABYTES;
diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java
index 7c3021d919abb7d99975f9da25c3e03de1cdc15e..59bf432a51bd1b5929d80204d731dc4c30847182 100644
--- a/components/net/sf/briar/db/Database.java
+++ b/components/net/sf/briar/db/Database.java
@@ -14,8 +14,8 @@ import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 
 /**
- * A low-level interface to the database that is managed by a
- * DatabaseComponent. Most operations take a transaction argument, which is
+ * A low-level interface to the database (DatabaseComponent provides a
+ * high-level interface). Most operations take a transaction argument, which is
  * obtained by calling startTransaction(). Every transaction must be
  * terminated by calling either abortTransaction() or commitTransaction(),
  * even if an exception is thrown.
diff --git a/components/net/sf/briar/db/H2Database.java b/components/net/sf/briar/db/H2Database.java
index 28ae4f6c70e86f5c63b129009fe7858f555447d5..9c6908a7dba8fb96791c0bb40d1082f459b47888 100644
--- a/components/net/sf/briar/db/H2Database.java
+++ b/components/net/sf/briar/db/H2Database.java
@@ -10,31 +10,32 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.crypto.Password;
-import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabasePassword;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.protocol.MessageFactory;
-import net.sf.briar.util.FileUtils;
 
 import com.google.inject.Inject;
 
+/** Contains all the H2-specific code for the database. */
 class H2Database extends JdbcDatabase {
 
 	private static final Logger LOG =
 		Logger.getLogger(H2Database.class.getName());
 
-	private final Password password;
 	private final File home;
+	private final Password password;
 	private final String url;
+	private final long maxSize;
 
 	@Inject
-	H2Database(MessageFactory messageFactory,
-			@DatabasePassword Password password) {
+	H2Database(File dir, MessageFactory messageFactory,
+			@DatabasePassword Password password, long maxSize) {
 		super(messageFactory, "BINARY(32)");
+		home = new File(dir, "db");
 		this.password = password;
-		home = new File(FileUtils.getBriarDirectory(), "Data/db/db");
 		url = "jdbc:h2:split:" + home.getPath()
 		+ ";CIPHER=AES;DB_CLOSE_ON_EXIT=false";
+		this.maxSize = maxSize;
 	}
 
 	public void open(boolean resume) throws DbException {
@@ -54,7 +55,7 @@ class H2Database extends JdbcDatabase {
 		File dir = home.getParentFile();
 		long free = dir.getFreeSpace();
 		long used = getDiskSpace(dir);
-		long quota = DatabaseComponent.MAX_DB_SIZE - used;
+		long quota = maxSize - used;
 		long min =  Math.min(free, quota);
 		if(LOG.isLoggable(Level.FINE)) LOG.fine("Free space: " + min);
 		return min;
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index 2f861175147ac152b1fffbbd215e52c3be03dc3e..c848cafa2a8f6edbb516522b19b2b6c5979d9052 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -31,6 +31,10 @@ import net.sf.briar.api.protocol.MessageFactory;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.util.FileUtils;
 
+/**
+ * A generic database implementation that can be used with any JDBC-compatible
+ * database library. (Tested with H2, Derby and HSQLDB.)
+ */
 abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_LOCAL_SUBSCRIPTIONS =
@@ -945,7 +949,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 				+ " ON messages.groupId = contactSubscriptions.groupId"
 				+ " JOIN statuses ON messages.messageId = statuses.messageId"
 				+ " WHERE contactSubscriptions.contactId = ?"
-				+ " AND statuses.contactId = ? AND status = ?";
+				+ " AND statuses.contactId = ? AND status = ?"
+				+ " AND sendability > ZERO()";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setInt(2, c.getInt());
diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java
index 7f8c92c3936a17e250f3d7daceed2039375a2bea..0ae46a8221f77244cf2c3f602aed55f2cb5c3707 100644
--- a/components/net/sf/briar/protocol/MessageImpl.java
+++ b/components/net/sf/briar/protocol/MessageImpl.java
@@ -5,7 +5,7 @@ import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 
-class MessageImpl implements Message {
+public class MessageImpl implements Message {
 
 	private final MessageId id, parent;
 	private final GroupId group;
diff --git a/test/build.xml b/test/build.xml
index 12f16f5501fb11b2c0f99c25fe5407ad101ec217..35e47c88d8326f360322ff91d52f7979b864a50c 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -13,6 +13,7 @@
 				<path refid='test-classes'/>
 				<path refid='util-classes'/>
 			</classpath>
+			<test name='net.sf.briar.db.H2DatabaseTest'/>
 			<test name='net.sf.briar.i18n.FontManagerTest'/>
 			<test name='net.sf.briar.i18n.I18nTest'/>
 			<test name='net.sf.briar.invitation.InvitationWorkerTest'/>
diff --git a/test/net/sf/briar/TestUtils.java b/test/net/sf/briar/TestUtils.java
index 32b8198f59aba43a6351db70b4f11b15fdca06ab..552fbd427e7ab0fd6a0c82a7bb494f6063b0c48d 100644
--- a/test/net/sf/briar/TestUtils.java
+++ b/test/net/sf/briar/TestUtils.java
@@ -30,8 +30,9 @@ public class TestUtils {
 		return testDir;
 	}
 
-	public static void deleteTestDirectories() {
-		delete(new File("test.tmp"));
+	public static void deleteTestDirectory(File testDir) {
+		delete(testDir);
+		testDir.getParentFile().delete(); // Delete if empty
 	}
 
 	public static File getBuildDirectory() {
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..00c029ffae837c0f5573c3fe0467d998704d747b
--- /dev/null
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -0,0 +1,620 @@
+package net.sf.briar.db;
+
+import java.io.File;
+import java.sql.Connection;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
+import net.sf.briar.api.crypto.Password;
+import net.sf.briar.api.db.ContactId;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.Rating;
+import net.sf.briar.api.db.Status;
+import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageFactory;
+import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.protocol.MessageImpl;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class H2DatabaseTest extends TestCase {
+
+	private static final int ONE_MEGABYTE = 1024 * 1024;
+	private static final int MAX_SIZE = 5 * ONE_MEGABYTE;
+
+	private final File testDir = TestUtils.getTestDirectory();
+	// The password has the format <file password> <space> <user password>
+	private final String passwordString = "foo bar";
+	// Some bytes for test IDs
+	private final byte[] idBytes = new byte[32], idBytes1 = new byte[32];
+	private final BatchId batchId;
+	private final ContactId contactId;
+	private final MessageId messageId;
+	private final GroupId groupId;
+	private final AuthorId authorId;
+	private final long timestamp = System.currentTimeMillis();
+	private final int size = 1234;
+	private final byte[] body = new byte[size];
+	private final Message message;
+
+	public H2DatabaseTest() {
+		super();
+		for(int i = 0; i < idBytes.length; i++) idBytes[i] = (byte) i;
+		for(int i = 0; i < idBytes1.length; i++) idBytes1[i] = (byte) (i + 1);
+		for(int i = 0; i < body.length; i++) body[i] = (byte) i;
+		batchId = new BatchId(idBytes);
+		contactId = new ContactId(123);
+		messageId = new MessageId(idBytes);
+		groupId = new GroupId(idBytes);
+		authorId = new AuthorId(idBytes);
+		message = new MessageImpl(messageId, MessageId.NONE, groupId, authorId,
+				timestamp, body);
+	}
+
+	@Before
+	public void setUp() {
+		testDir.mkdirs();
+	}
+
+	@Test
+	public void testPersistence() throws DbException {
+		MessageFactory messageFactory = new TestMessageFactory();
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Store some records
+		Connection txn = db.startTransaction();
+		assertFalse(db.containsContact(txn, contactId));
+		db.addContact(txn, contactId);
+		assertTrue(db.containsContact(txn, contactId));
+		assertFalse(db.containsSubscription(txn, groupId));
+		db.addSubscription(txn, groupId);
+		assertTrue(db.containsSubscription(txn, groupId));
+		assertFalse(db.containsMessage(txn, messageId));
+		db.addMessage(txn, message);
+		assertTrue(db.containsMessage(txn, messageId));
+		db.commitTransaction(txn);
+		db.close();
+
+		// Reopen the database
+		db = open(true, messageFactory);
+		// Check that the records are still there
+		txn = db.startTransaction();
+		assertTrue(db.containsContact(txn, contactId));
+		assertTrue(db.containsSubscription(txn, groupId));
+		assertTrue(db.containsMessage(txn, messageId));
+		Message m1 = db.getMessage(txn, messageId);
+		assertEquals(messageId, m1.getId());
+		assertEquals(MessageId.NONE, m1.getParent());
+		assertEquals(groupId, m1.getGroup());
+		assertEquals(authorId, m1.getAuthor());
+		assertEquals(timestamp, m1.getTimestamp());
+		assertEquals(size, m1.getSize());
+		assertTrue(Arrays.equals(body, m1.getBody()));
+		// Delete the records
+		db.removeContact(txn, contactId);
+		db.removeMessage(txn, messageId);
+		db.removeSubscription(txn, groupId);
+		db.commitTransaction(txn);
+		db.close();
+
+		// Repoen the database
+		db = open(true, messageFactory);
+		// Check that the records are gone
+		txn = db.startTransaction();
+		assertFalse(db.containsContact(txn, contactId));
+		assertFalse(db.containsSubscription(txn, groupId));
+		assertFalse(db.containsMessage(txn, messageId));
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testRatings() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+		Database<Connection> db = open(false, messageFactory);
+		Connection txn = db.startTransaction();
+		// Unknown authors should be unrated
+		assertEquals(Rating.UNRATED, db.getRating(txn, authorId));
+		// Store a rating
+		db.setRating(txn, authorId, Rating.GOOD);
+		db.commitTransaction(txn);
+		// Check that the rating was stored
+		txn = db.startTransaction();
+		assertEquals(Rating.GOOD, db.getRating(txn, authorId));
+		db.commitTransaction(txn);
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testUnsubscribingRemovesMessage() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addSubscription(txn, groupId);
+		db.addMessage(txn, message);
+		db.commitTransaction(txn);
+
+		// Unsubscribing from the group should delete the message
+		txn = db.startTransaction();
+		assertTrue(db.containsMessage(txn, messageId));
+		db.removeSubscription(txn, groupId);
+		assertFalse(db.containsMessage(txn, messageId));
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeSendable() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact, subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addSubscription(txn, groupId);
+		db.addSubscription(txn, contactId, groupId);
+		db.addMessage(txn, message);
+		db.setStatus(txn, contactId, messageId, Status.NEW);
+		db.commitTransaction(txn);
+
+		// The message should not be sendable
+		txn = db.startTransaction();
+		assertEquals(0, db.getSendability(txn, messageId));
+		Iterator<MessageId> it =
+			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		// Changing the sendability to > 0 should make the message sendable
+		txn = db.startTransaction();
+		db.setSendability(txn, messageId, 1);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		db.commitTransaction(txn);
+
+		// Changing the sendability to 0 should make the message unsendable
+		txn = db.startTransaction();
+		db.setSendability(txn, messageId, 0);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeNew() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact, subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addSubscription(txn, groupId);
+		db.addSubscription(txn, contactId, groupId);
+		db.addMessage(txn, message);
+		db.setSendability(txn, messageId, 1);
+		db.commitTransaction(txn);
+
+		// The message has no status yet, so it should not be sendable
+		txn = db.startTransaction();
+		Iterator<MessageId> it =
+			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		// Changing the status to Status.NEW should make the message sendable
+		txn = db.startTransaction();
+		db.setStatus(txn, contactId, messageId, Status.NEW);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		db.commitTransaction(txn);
+
+		// Changing the status to SENT should make the message unsendable
+		txn = db.startTransaction();
+		db.setStatus(txn, contactId, messageId, Status.SENT);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		// Changing the status to SEEN should also make the message unsendable
+		txn = db.startTransaction();
+		db.setStatus(txn, contactId, messageId, Status.SEEN);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeSubscribed() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact, subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addSubscription(txn, groupId);
+		db.addMessage(txn, message);
+		db.setSendability(txn, messageId, 1);
+		db.setStatus(txn, contactId, messageId, Status.NEW);
+		db.commitTransaction(txn);
+
+		// The contact is not subscribed, so the message should not be sendable
+		txn = db.startTransaction();
+		Iterator<MessageId> it =
+			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		// The contact subscribing should make the message sendable
+		txn = db.startTransaction();
+		db.addSubscription(txn, contactId, groupId);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		db.commitTransaction(txn);
+
+		// The contact unsubscribing should make the message unsendable
+		txn = db.startTransaction();
+		db.clearSubscriptions(txn, contactId);
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testSendableMessagesMustFitCapacity() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact, subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addSubscription(txn, groupId);
+		db.addSubscription(txn, contactId, groupId);
+		db.addMessage(txn, message);
+		db.setSendability(txn, messageId, 1);
+		db.setStatus(txn, contactId, messageId, Status.NEW);
+		db.commitTransaction(txn);
+
+		// The message is too large to send
+		txn = db.startTransaction();
+		Iterator<MessageId> it =
+			db.getSendableMessages(txn, contactId, size - 1).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		// The message is just the right size to send
+		txn = db.startTransaction();
+		it = db.getSendableMessages(txn, contactId, size).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testBatchesToAck() throws DbException {
+		BatchId batchId1 = new BatchId(idBytes1);
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact and some batches to ack
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addBatchToAck(txn, contactId, batchId);
+		db.addBatchToAck(txn, contactId, batchId1);
+		db.commitTransaction(txn);
+
+		// Both batch IDs should be returned
+		txn = db.startTransaction();
+		Set<BatchId> acks = db.removeBatchesToAck(txn, contactId);
+		assertEquals(2, acks.size());
+		assertTrue(acks.contains(batchId));
+		assertTrue(acks.contains(batchId1));
+		db.commitTransaction(txn);
+
+		// Both batch IDs should have been removed
+		txn = db.startTransaction();
+		acks = db.removeBatchesToAck(txn, contactId);
+		assertEquals(0, acks.size());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testRemoveAckedBatch() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact, subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addSubscription(txn, groupId);
+		db.addSubscription(txn, contactId, groupId);
+		db.addMessage(txn, message);
+		db.setSendability(txn, messageId, 1);
+		db.setStatus(txn, contactId, messageId, Status.NEW);
+		db.commitTransaction(txn);
+
+		// Get the message and mark it as sent
+		txn = db.startTransaction();
+		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.addOutstandingBatch(txn, contactId, batchId,
+				Collections.singleton(messageId));
+		db.commitTransaction(txn);
+
+		// The message should no longer be sendable
+		txn = db.startTransaction();
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		// Pretend that the batch was acked
+		db.removeAckedBatch(txn, contactId, batchId);
+		// The message still should not be sendable
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testRemoveLostBatch() throws DbException {
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Add a contact, subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addContact(txn, contactId);
+		db.addSubscription(txn, groupId);
+		db.addSubscription(txn, contactId, groupId);
+		db.addMessage(txn, message);
+		db.setSendability(txn, messageId, 1);
+		db.setStatus(txn, contactId, messageId, Status.NEW);
+		db.commitTransaction(txn);
+
+		// Get the message and mark it as sent
+		txn = db.startTransaction();
+		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.addOutstandingBatch(txn, contactId, batchId,
+				Collections.singleton(messageId));
+		db.commitTransaction(txn);
+
+		// The message should no longer be sendable
+		txn = db.startTransaction();
+		it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+		assertFalse(it.hasNext());
+		// Pretend that the batch was lost
+		db.removeLostBatch(txn, contactId, batchId);
+		// 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();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testGetMessagesByAuthor() throws DbException {
+		AuthorId authorId1 = new AuthorId(idBytes1);
+		MessageId messageId1 = new MessageId(idBytes1);
+		Message message1 = new MessageImpl(messageId1, MessageId.NONE, groupId,
+				authorId1, timestamp, body);
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Subscribe to a group and store two messages
+		Connection txn = db.startTransaction();
+		db.addSubscription(txn, groupId);
+		db.addMessage(txn, message);
+		db.addMessage(txn, message1);
+		db.commitTransaction(txn);
+
+		// Check that each message is retrievable via its author
+		txn = db.startTransaction();
+		Iterator<MessageId> it =
+			db.getMessagesByAuthor(txn, authorId).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		assertFalse(it.hasNext());
+		it = db.getMessagesByAuthor(txn, authorId1).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId1, it.next());
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testGetMessagesByParent() throws DbException {
+		MessageId parentId = new MessageId(idBytes1);
+		Message message1 = new MessageImpl(messageId, parentId, groupId,
+				authorId, timestamp, body);
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Subscribe to a group and store a message
+		Connection txn = db.startTransaction();
+		db.addSubscription(txn, groupId);
+		db.addMessage(txn, message1);
+		db.commitTransaction(txn);
+
+		// Check that the message is retrievable via its parent
+		txn = db.startTransaction();
+		Iterator<MessageId> it =
+			db.getMessagesByParent(txn, parentId).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testGetOldMessages() throws DbException {
+		MessageId messageId1 = new MessageId(idBytes1);
+		Message message1 = new MessageImpl(messageId1, MessageId.NONE, groupId,
+				authorId, timestamp + 1000, body);
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Subscribe to a group and store two messages
+		Connection txn = db.startTransaction();
+		db.addSubscription(txn, groupId);
+		db.addMessage(txn, message);
+		db.addMessage(txn, message1);
+		db.commitTransaction(txn);
+
+		// Allowing enough capacity for one message should return the older one
+		txn = db.startTransaction();
+		Iterator<MessageId> it = db.getOldMessages(txn, size).iterator();
+		assertTrue(it.hasNext());
+		assertEquals(messageId, it.next());
+		assertFalse(it.hasNext());
+		db.commitTransaction(txn);
+
+		// Allowing enough capacity for both messages should return both
+		txn = db.startTransaction();
+		Set<MessageId> ids = new HashSet<MessageId>();
+		for(MessageId id : db.getOldMessages(txn, size * 2)) ids.add(id);
+		assertEquals(2, ids.size());
+		assertTrue(ids.contains(messageId));
+		assertTrue(ids.contains(messageId1));
+		db.commitTransaction(txn);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testGetFreeSpace() throws DbException {
+		byte[] largeBody = new byte[ONE_MEGABYTE];
+		for(int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i;
+		Message message1 = new MessageImpl(messageId, MessageId.NONE, groupId,
+				authorId, timestamp, largeBody);
+		Mockery context = new Mockery();
+		MessageFactory messageFactory = context.mock(MessageFactory.class);
+
+		// Create a new database
+		Database<Connection> db = open(false, messageFactory);
+		// Sanity check: there should be enough space on disk for this test
+		assertTrue(testDir.getFreeSpace() > MAX_SIZE);
+		// The free space should not be more than the allowed maximum size
+		long free = db.getFreeSpace();
+		assertTrue(free <= MAX_SIZE);
+		assertTrue(free > 0);
+		// Storing a message should reduce the free space
+		Connection txn = db.startTransaction();
+		db.addSubscription(txn, groupId);
+		db.addMessage(txn, message1);
+		db.commitTransaction(txn);
+		assertTrue(db.getFreeSpace() < free);
+
+		db.close();
+		context.assertIsSatisfied();
+	}
+
+	private Database<Connection> open(boolean resume,
+			MessageFactory messageFactory) throws DbException {
+		final char[] passwordArray = passwordString.toCharArray();
+		Mockery context = new Mockery();
+		final Password password = context.mock(Password.class);
+		context.checking(new Expectations() {{
+			oneOf(password).getPassword();
+			will(returnValue(passwordArray));
+		}});
+		Database<Connection> db =
+			new H2Database(testDir, messageFactory, password, MAX_SIZE);
+		db.open(resume);
+		context.assertIsSatisfied();
+		// The password array should be cleared after use
+		assertTrue(Arrays.equals(new char[passwordString.length()],
+				passwordArray));
+		return db;
+	}
+
+	@After
+	public void tearDown() {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+
+	private static class TestMessageFactory implements MessageFactory {
+
+		public Message createMessage(MessageId id, MessageId parent,
+				GroupId group, AuthorId author, long timestamp, byte[] body) {
+			return new MessageImpl(id, parent, group, author, timestamp, body);
+		}
+	}
+}
diff --git a/test/net/sf/briar/i18n/I18nTest.java b/test/net/sf/briar/i18n/I18nTest.java
index 23e9c8fa562010319970f54ace154672f1c0e78e..1b0cd5e18f42126dc476da280dd964e6ee2a4720 100644
--- a/test/net/sf/briar/i18n/I18nTest.java
+++ b/test/net/sf/briar/i18n/I18nTest.java
@@ -97,6 +97,6 @@ public class I18nTest extends TestCase {
 	public void tearDown() {
 		TestUtils.delete(base);
 		TestUtils.delete(french);
-		TestUtils.deleteTestDirectories();
+		TestUtils.deleteTestDirectory(testDir);
 	}
 }
diff --git a/test/net/sf/briar/invitation/InvitationWorkerTest.java b/test/net/sf/briar/invitation/InvitationWorkerTest.java
index 3090f71f67c48129e681dd08e52dfb6b1dcb109d..482e0ac93fa4eb280b915b97bae09484c1797563 100644
--- a/test/net/sf/briar/invitation/InvitationWorkerTest.java
+++ b/test/net/sf/briar/invitation/InvitationWorkerTest.java
@@ -148,6 +148,6 @@ public class InvitationWorkerTest extends TestCase {
 
 	@After
 	public void tearDown() {
-		TestUtils.deleteTestDirectories();
+		TestUtils.deleteTestDirectory(testDir);
 	}
 }
diff --git a/test/net/sf/briar/setup/SetupWorkerTest.java b/test/net/sf/briar/setup/SetupWorkerTest.java
index c597d9cf94784e1d701ddd4c475cc15cebe2081d..5300bc87646e19e940690be6e94f1ef7958b0bf9 100644
--- a/test/net/sf/briar/setup/SetupWorkerTest.java
+++ b/test/net/sf/briar/setup/SetupWorkerTest.java
@@ -166,6 +166,6 @@ public class SetupWorkerTest extends TestCase {
 
 	@After
 	public void tearDown() {
-		TestUtils.deleteTestDirectories();
+		TestUtils.deleteTestDirectory(testDir);
 	}
 }
diff --git a/test/net/sf/briar/util/FileUtilsTest.java b/test/net/sf/briar/util/FileUtilsTest.java
index 8874a306b8b69698104d12aac67dd7537bb7dbc5..70e14c0f2d01b291c087b9f60e32d72da06baf8f 100644
--- a/test/net/sf/briar/util/FileUtilsTest.java
+++ b/test/net/sf/briar/util/FileUtilsTest.java
@@ -160,6 +160,6 @@ public class FileUtilsTest extends TestCase {
 
 	@After
 	public void tearDown() {
-		TestUtils.deleteTestDirectories();
+		TestUtils.deleteTestDirectory(testDir);
 	}
 }
diff --git a/test/net/sf/briar/util/ZipUtilsTest.java b/test/net/sf/briar/util/ZipUtilsTest.java
index 613c87cd74cd6ff7a3ec84bf6a0bdfb4aff94b13..85f5e1259596a961169fdbbd335bc768b1e159a8 100644
--- a/test/net/sf/briar/util/ZipUtilsTest.java
+++ b/test/net/sf/briar/util/ZipUtilsTest.java
@@ -196,6 +196,6 @@ public class ZipUtilsTest extends TestCase {
 
 	@After
 	public void tearDown() {
-		TestUtils.deleteTestDirectories();
+		TestUtils.deleteTestDirectory(testDir);
 	}
 }