diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java
index 59c88d4d7fe03b492aad603783d4718ee1abf50b..1714a49ea9b6861671d83be2e1313cbbe423262c 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java
@@ -2,13 +2,30 @@ package org.briarproject;
 
 import junit.framework.Assert;
 
-import org.briarproject.api.db.DatabaseComponent;
+import net.jodah.concurrentunit.Waiter;
+
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPost;
 import org.briarproject.api.forum.ForumPostFactory;
 import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.SyncSession;
+import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.api.system.Clock;
 import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.forum.ForumModule;
@@ -17,55 +34,101 @@ import org.briarproject.properties.PropertiesModule;
 import org.briarproject.sync.SyncModule;
 import org.briarproject.transport.TransportModule;
 import org.briarproject.util.StringUtils;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.IOException;
 import java.util.Collection;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.TestCase.assertFalse;
+import static org.briarproject.TestPluginsModule.MAX_LATENCY;
+import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
 import static org.junit.Assert.assertTrue;
 
 public class ForumManagerTest {
 
+	private LifecycleManager lifecycleManager0, lifecycleManager1;
+	private SyncSessionFactory sync0, sync1;
+	private ForumManager forumManager0, forumManager1;
+	private ContactManager contactManager0, contactManager1;
+	private ContactId contactId0,contactId1;
+	private IdentityManager identityManager0, identityManager1;
+	private LocalAuthor author0, author1;
+	private Forum forum0;
+
 	@Inject
-	protected ForumManager forumManager;
+	Clock clock;
 	@Inject
-	protected ForumPostFactory forumPostFactory;
+	AuthorFactory authorFactory;
 	@Inject
-	protected DatabaseComponent db;
+	ForumPostFactory forumPostFactory;
+
+	// objects accessed from background threads need to be volatile
+	private volatile ForumSharingManager forumSharingManager0;
+	private volatile ForumSharingManager forumSharingManager1;
+	private volatile Waiter validationWaiter;
+	private volatile Waiter deliveryWaiter;
 
 	private final File testDir = TestUtils.getTestDirectory();
+	private final SecretKey master = TestUtils.getSecretKey();
+	private final int TIMEOUT = 15000;
+	private final String SHARER = "Sharer";
+	private final String INVITEE = "Invitee";
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumSharingIntegrationTest.class.getName());
+
+	private ForumManagerTestComponent t0, t1;
 
 	@Before
 	public void setUp() throws Exception {
+		ForumManagerTestComponent component =
+				DaggerForumManagerTestComponent.builder().build();
+		component.inject(this);
+		injectEagerSingletons(component);
 
 		assertTrue(testDir.mkdirs());
-		File tDir = new File(testDir, "db");
+		File t0Dir = new File(testDir, SHARER);
+		t0 = DaggerForumManagerTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
+		injectEagerSingletons(t0);
+		File t1Dir = new File(testDir, INVITEE);
+		t1 = DaggerForumManagerTestComponent.builder()
+				.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
+		injectEagerSingletons(t1);
 
-		ForumManagerTestComponent component =
-				DaggerForumManagerTestComponent.builder()
-						.testDatabaseModule(new TestDatabaseModule(tDir))
-						.build();
+		identityManager0 = t0.getIdentityManager();
+		identityManager1 = t1.getIdentityManager();
+		contactManager0 = t0.getContactManager();
+		contactManager1 = t1.getContactManager();
+		forumManager0 = t0.getForumManager();
+		forumManager1 = t1.getForumManager();
+		forumSharingManager0 = t0.getForumSharingManager();
+		forumSharingManager1 = t1.getForumSharingManager();
+		sync0 = t0.getSyncSessionFactory();
+		sync1 = t1.getSyncSessionFactory();
 
-		component.inject(new LifecycleModule.EagerSingletons());
-		component.inject(new ForumModule.EagerSingletons());
-		component.inject(new CryptoModule.EagerSingletons());
-		component.inject(new ContactModule.EagerSingletons());
-		component.inject(new TransportModule.EagerSingletons());
-		component.inject(new SyncModule.EagerSingletons());
-		component.inject(new PropertiesModule.EagerSingletons());
-		component.inject(this);
+		// initialize waiters fresh for each test
+		validationWaiter = new Waiter();
+		deliveryWaiter = new Waiter();
 	}
 
-	ForumPost createForumPost(GroupId groupId, ForumPost parent, String body,
-			long ms)
-			throws Exception {
+	private ForumPost createForumPost(GroupId groupId, ForumPost parent,
+			String body, long ms) throws Exception {
 		return forumPostFactory.createAnonymousPost(groupId, ms,
 				parent == null ? null : parent.getMessage().getId(),
 				"text/plain", StringUtils.toUtf8(body));
@@ -73,13 +136,12 @@ public class ForumManagerTest {
 
 	@Test
 	public void testForumPost() throws Exception {
-		assertFalse(db.open());
-		assertNotNull(forumManager);
-		Forum forum = forumManager.addForum("TestForum");
-		assertEquals(1, forumManager.getForums().size());
-		final long ms1 = System.currentTimeMillis() - 1000L;
+		startLifecycles();
+		Forum forum = forumManager0.addForum("TestForum");
+		assertEquals(1, forumManager0.getForums().size());
+		final long ms1 = clock.currentTimeMillis() - 1000L;
 		final String body1 = "some forum text";
-		final long ms2 = System.currentTimeMillis();
+		final long ms2 = clock.currentTimeMillis();
 		final String body2 = "some other forum text";
 		ForumPost post1 =
 				createForumPost(forum.getGroup().getId(), null, body1, ms1);
@@ -87,16 +149,16 @@ public class ForumManagerTest {
 		ForumPost post2 =
 				createForumPost(forum.getGroup().getId(), post1, body2, ms2);
 		assertEquals(ms2, post2.getMessage().getTimestamp());
-		forumManager.addLocalPost(post1);
-		forumManager.setReadFlag(post1.getMessage().getId(), true);
-		forumManager.addLocalPost(post2);
-		forumManager.setReadFlag(post2.getMessage().getId(), false);
+		forumManager0.addLocalPost(post1);
+		forumManager0.setReadFlag(post1.getMessage().getId(), true);
+		forumManager0.addLocalPost(post2);
+		forumManager0.setReadFlag(post2.getMessage().getId(), false);
 		Collection<ForumPostHeader> headers =
-				forumManager.getPostHeaders(forum.getGroup().getId());
+				forumManager0.getPostHeaders(forum.getGroup().getId());
 		assertEquals(2, headers.size());
 		for (ForumPostHeader h : headers) {
 			final String hBody =
-					StringUtils.fromUtf8(forumManager.getPostBody(h.getId()));
+					StringUtils.fromUtf8(forumManager0.getPostBody(h.getId()));
 
 			boolean isPost1 = h.getId().equals(post1.getMessage().getId());
 			boolean isPost2 = h.getId().equals(post2.getMessage().getId());
@@ -114,8 +176,258 @@ public class ForumManagerTest {
 				assertFalse(h.isRead());
 			}
 		}
-		forumManager.removeForum(forum);
-		assertEquals(0, forumManager.getForums().size());
-		db.close();
+		forumManager0.removeForum(forum);
+		assertEquals(0, forumManager0.getForums().size());
+		stopLifecycles();
 	}
+
+	@Test
+	public void testForumPostDelivery() throws Exception {
+		startLifecycles();
+		defaultInit();
+
+		// share forum
+		GroupId g = forum0.getId();
+		forumSharingManager0.sendForumInvitation(g, contactId1, null);
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		Contact c0 = contactManager1.getContact(contactId0);
+		forumSharingManager1.respondToInvitation(forum0, c0, true);
+		sync1To0();
+		deliveryWaiter.await(TIMEOUT, 1);
+
+		// add one forum post
+		long time = clock.currentTimeMillis();
+		ForumPost post1 = createForumPost(g, null, "a", time);
+		forumManager0.addLocalPost(post1);
+		assertEquals(1, forumManager0.getPostHeaders(g).size());
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+
+		// send post to 1
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		assertEquals(1, forumManager1.getPostHeaders(g).size());
+
+		stopLifecycles();
+	}
+
+	@Test
+	public void testForumPostDeliveredAfterParent() throws Exception {
+		startLifecycles();
+		defaultInit();
+
+		// share forum
+		GroupId g = forum0.getId();
+		forumSharingManager0.sendForumInvitation(g, contactId1, null);
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		Contact c0 = contactManager1.getContact(contactId0);
+		forumSharingManager1.respondToInvitation(forum0, c0, true);
+		sync1To0();
+		deliveryWaiter.await(TIMEOUT, 1);
+
+		// add one forum post without the parent
+		long time = clock.currentTimeMillis();
+		ForumPost post1 = createForumPost(g, null, "a", time);
+		ForumPost post2 = createForumPost(g, post1, "a", time);
+		forumManager0.addLocalPost(post2);
+		assertEquals(1, forumManager0.getPostHeaders(g).size());
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+
+		// send post to 1 without waiting for message delivery
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+
+		// now add the parent post as well
+		forumManager0.addLocalPost(post1);
+		assertEquals(2, forumManager0.getPostHeaders(g).size());
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+
+		// and send it over to 1 and wait for a second message to be delivered
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 2);
+		assertEquals(2, forumManager1.getPostHeaders(g).size());
+
+		stopLifecycles();
+	}
+
+	@Test
+	public void testForumPostWithParentInOtherGroup() throws Exception {
+		startLifecycles();
+		defaultInit();
+
+		// share forum
+		GroupId g = forum0.getId();
+		forumSharingManager0.sendForumInvitation(g, contactId1, null);
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		Contact c0 = contactManager1.getContact(contactId0);
+		forumSharingManager1.respondToInvitation(forum0, c0, true);
+		sync1To0();
+		deliveryWaiter.await(TIMEOUT, 1);
+
+		// share a second forum
+		Forum forum1 = forumManager0.addForum("Test Forum1");
+		GroupId g1 = forum1.getId();
+		forumSharingManager0.sendForumInvitation(g1, contactId1, null);
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		forumSharingManager1.respondToInvitation(forum1, c0, true);
+		sync1To0();
+		deliveryWaiter.await(TIMEOUT, 1);
+
+		// add one forum post with a parent in another forum
+		long time = clock.currentTimeMillis();
+		ForumPost post1 = createForumPost(g1, null, "a", time);
+		ForumPost post = createForumPost(g, post1, "b", time);
+		forumManager0.addLocalPost(post);
+		assertEquals(1, forumManager0.getPostHeaders(g).size());
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+
+		// send posts to 1
+		sync0To1();
+		validationWaiter.await(TIMEOUT, 1);
+		assertEquals(1, forumManager0.getPostHeaders(g).size());
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+
+		// now also add the parent post which is in another group
+		forumManager0.addLocalPost(post1);
+		assertEquals(1, forumManager0.getPostHeaders(g1).size());
+		assertEquals(0, forumManager1.getPostHeaders(g1).size());
+
+		// send posts to 1
+		sync0To1();
+		deliveryWaiter.await(TIMEOUT, 1);
+		assertEquals(1, forumManager0.getPostHeaders(g).size());
+		assertEquals(1, forumManager0.getPostHeaders(g1).size());
+		// the next line is critical, makes sure post doesn't show up
+		assertEquals(0, forumManager1.getPostHeaders(g).size());
+		assertEquals(1, forumManager1.getPostHeaders(g1).size());
+
+		stopLifecycles();
+	}
+
+	@After
+	public void tearDown() throws Exception {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+
+	private class Listener implements EventListener {
+		public void eventOccurred(Event e) {
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				if (!event.isLocal()) {
+					if (event.getState() == DELIVERED) {
+						deliveryWaiter.resume();
+					} else if (event.getState() == VALID ||
+							event.getState() == INVALID ||
+							event.getState() == PENDING) {
+						validationWaiter.resume();
+					}
+				}
+			}
+		}
+	}
+
+	private void defaultInit() throws DbException {
+		addDefaultIdentities();
+		addDefaultContacts();
+		addForum();
+		listenToEvents();
+	}
+
+	private void addDefaultIdentities() throws DbException {
+		author0 = authorFactory.createLocalAuthor(SHARER,
+				TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				TestUtils.getRandomBytes(123));
+		identityManager0.addLocalAuthor(author0);
+		author1 = authorFactory.createLocalAuthor(INVITEE,
+				TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
+				TestUtils.getRandomBytes(123));
+		identityManager1.addLocalAuthor(author1);
+	}
+
+	private void addDefaultContacts() throws DbException {
+		// sharer adds invitee as contact
+		contactId1 = contactManager0.addContact(author1,
+				author0.getId(), master, clock.currentTimeMillis(), true,
+				true
+		);
+		// invitee adds sharer back
+		contactId0 = contactManager1.addContact(author0,
+				author1.getId(), master, clock.currentTimeMillis(), true,
+				true
+		);
+	}
+
+	private void addForum() throws DbException {
+		forum0 = forumManager0.addForum("Test Forum");
+	}
+
+	private void listenToEvents() {
+		Listener listener0 = new Listener();
+		t0.getEventBus().addListener(listener0);
+		Listener listener1 = new Listener();
+		t1.getEventBus().addListener(listener1);
+	}
+
+	private void sync0To1() throws IOException, TimeoutException {
+		deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
+	}
+
+	private void sync1To0() throws IOException, TimeoutException {
+		deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
+	}
+
+	private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
+			SyncSessionFactory toSync, ContactId toId, String debug)
+			throws IOException, TimeoutException {
+
+		if (debug != null) LOG.info("TEST: Sending message from " + debug);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		// Create an outgoing sync session
+		SyncSession sessionFrom =
+				fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
+		// Write whatever needs to be written
+		sessionFrom.run();
+		out.close();
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		// Create an incoming sync session
+		SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
+		// Read whatever needs to be read
+		sessionTo.run();
+		in.close();
+	}
+
+	private void startLifecycles() throws InterruptedException {
+		// Start the lifecycle manager and wait for it to finish
+		lifecycleManager0 = t0.getLifecycleManager();
+		lifecycleManager1 = t1.getLifecycleManager();
+		lifecycleManager0.startServices();
+		lifecycleManager1.startServices();
+		lifecycleManager0.waitForStartup();
+		lifecycleManager1.waitForStartup();
+	}
+
+	private void stopLifecycles() throws InterruptedException {
+		// Clean up
+		lifecycleManager0.stopServices();
+		lifecycleManager1.stopServices();
+		lifecycleManager0.waitForShutdown();
+		lifecycleManager1.waitForShutdown();
+	}
+
+	private void injectEagerSingletons(ForumManagerTestComponent component) {
+		component.inject(new LifecycleModule.EagerSingletons());
+		component.inject(new ForumModule.EagerSingletons());
+		component.inject(new CryptoModule.EagerSingletons());
+		component.inject(new ContactModule.EagerSingletons());
+		component.inject(new TransportModule.EagerSingletons());
+		component.inject(new SyncModule.EagerSingletons());
+		component.inject(new PropertiesModule.EagerSingletons());
+	}
+
 }
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
index cac69fb0df0754b6af1ada181fb29512d27d2e8b..8a9e5fc6db41a653e6e9737a39874cb89dce6b4c 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
@@ -1,5 +1,12 @@
 package org.briarproject;
 
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sync.SyncSessionFactory;
 import org.briarproject.clients.ClientsModule;
 import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
@@ -55,4 +62,18 @@ public interface ForumManagerTestComponent {
 
 	void inject(TransportModule.EagerSingletons init);
 
+	LifecycleManager getLifecycleManager();
+
+	EventBus getEventBus();
+
+	IdentityManager getIdentityManager();
+
+	ContactManager getContactManager();
+
+	ForumSharingManager getForumSharingManager();
+
+	ForumManager getForumManager();
+
+	SyncSessionFactory getSyncSessionFactory();
+
 }
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
index 87cfec0977269ba321f496c532fa72d616a56e7a..79809f444b7fa97999576616b48fa58182f7278e 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
@@ -18,7 +18,7 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.ForumInvitationReceivedEvent;
 import org.briarproject.api.event.ForumInvitationResponseReceivedEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumInvitationMessage;
 import org.briarproject.api.forum.ForumManager;
@@ -27,9 +27,12 @@ import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.SyncSession;
 import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.api.sync.ValidationManager;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.api.system.Clock;
 import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
@@ -60,6 +63,8 @@ import static org.briarproject.TestPluginsModule.MAX_LATENCY;
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
 import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -669,14 +674,15 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
 		public volatile boolean responseReceived = false;
 
 		public void eventOccurred(Event e) {
-			if (e instanceof MessageValidatedEvent) {
-				MessageValidatedEvent event = (MessageValidatedEvent) e;
-				if (event.getClientId()
-						.equals(forumSharingManager0.getClientId()) &&
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				State s = event.getState();
+				ClientId c = event.getClientId();
+				if ((s == DELIVERED || s == INVALID) &&
+						c.equals(forumSharingManager0.getClientId()) &&
 						!event.isLocal()) {
 					LOG.info("TEST: Sharer received message in group " +
-							((MessageValidatedEvent) e).getMessage()
-									.getGroupId().hashCode());
+							event.getMessage().getGroupId().hashCode());
 					msgWaiter.resume();
 				}
 			} else if (e instanceof ForumInvitationResponseReceivedEvent) {
@@ -722,14 +728,15 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
 		}
 
 		public void eventOccurred(Event e) {
-			if (e instanceof MessageValidatedEvent) {
-				MessageValidatedEvent event = (MessageValidatedEvent) e;
-				if (event.getClientId()
-						.equals(forumSharingManager1.getClientId()) &&
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				State s = event.getState();
+				ClientId c = event.getClientId();
+				if ((s == DELIVERED || s == INVALID) &&
+						c.equals(forumSharingManager0.getClientId()) &&
 						!event.isLocal()) {
 					LOG.info("TEST: Invitee received message in group " +
-							((MessageValidatedEvent) e).getMessage()
-									.getGroupId().hashCode());
+							event.getMessage().getGroupId().hashCode());
 					msgWaiter.resume();
 				}
 			} else if (e instanceof ForumInvitationReceivedEvent) {
diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java
index 259be1a9d4f5295d7ab36be98822125757cae24e..52fad50bbfb83256fd3af44f4e9b6c4ed827d970 100644
--- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java
@@ -19,7 +19,7 @@ import org.briarproject.api.event.IntroductionAbortedEvent;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
 import org.briarproject.api.event.IntroductionSucceededEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
@@ -29,10 +29,13 @@ import org.briarproject.api.introduction.IntroductionRequest;
 import org.briarproject.api.lifecycle.LifecycleManager;
 import org.briarproject.api.properties.TransportProperties;
 import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.SyncSession;
 import org.briarproject.api.sync.SyncSessionFactory;
+import org.briarproject.api.sync.ValidationManager;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.api.system.Clock;
 import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
@@ -70,6 +73,8 @@ import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY
 import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -1047,15 +1052,16 @@ public class IntroductionIntegrationTest extends BriarTestCase {
 
 		@Override
 		public void eventOccurred(Event e) {
-			if (e instanceof MessageValidatedEvent) {
-				MessageValidatedEvent event = (MessageValidatedEvent) e;
-				if (event.getClientId()
-						.equals(introductionManager0.getClientId()) &&
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				State s = event.getState();
+				ClientId c = event.getClientId();
+				if ((s == DELIVERED || s == INVALID) &&
+						c.equals(introductionManager0.getClientId()) &&
 						!event.isLocal()) {
 					LOG.info("TEST: Introducee" + introducee +
 							" received message in group " +
-							((MessageValidatedEvent) e).getMessage()
-									.getGroupId().hashCode());
+							event.getMessage().getGroupId().hashCode());
 					msgWaiter.resume();
 				}
 			} else if (e instanceof IntroductionRequestReceivedEvent) {
@@ -1114,14 +1120,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
 
 		@Override
 		public void eventOccurred(Event e) {
-			if (e instanceof MessageValidatedEvent) {
-				MessageValidatedEvent event = (MessageValidatedEvent) e;
-				if (event.getClientId()
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				if (event.getState() == DELIVERED && event.getClientId()
 						.equals(introductionManager0.getClientId()) &&
 						!event.isLocal()) {
 					LOG.info("TEST: Introducer received message in group " +
-							((MessageValidatedEvent) e).getMessage()
-									.getGroupId().hashCode());
+							event.getMessage().getGroupId().hashCode());
 					msgWaiter.resume();
 				}
 			} else if (e instanceof IntroductionResponseReceivedEvent) {
diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
index 166c8e84fb05ba68632d9d2921fe321bdc43fbf6..7e3d83c9df06f3710fdb934af8c1a44502bba1f9 100644
--- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
@@ -24,7 +24,7 @@ import org.briarproject.api.event.ForumInvitationReceivedEvent;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
 import org.briarproject.api.event.IntroductionSucceededEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.lifecycle.Service;
@@ -59,6 +59,7 @@ import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.BriarActivity.GROUP_ID;
 import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 
 class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		Service, EventListener {
@@ -156,9 +157,9 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		if (e instanceof SettingsUpdatedEvent) {
 			SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
 			if (s.getNamespace().equals(SETTINGS_NAMESPACE)) loadSettings();
-		} else if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
-			if (m.isValid() && !m.isLocal()) {
+		} else if (e instanceof MessageStateChangedEvent) {
+			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
+			if (!m.isLocal() && m.getState() == DELIVERED) {
 				ClientId c = m.getClientId();
 				if (c.equals(messagingManager.getClientId()))
 					showPrivateMessageNotification(m.getMessage().getGroupId());
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index d70f7d439cc2dd2facbcd91f45daeb8d71605c3d..7b21f28ac2950306b06673f50b64f200bd766971 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -30,7 +30,7 @@ import org.briarproject.api.event.ContactStatusChangedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.forum.ForumInvitationMessage;
 import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.identity.IdentityManager;
@@ -54,6 +54,7 @@ import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAn
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.BriarActivity.GROUP_ID;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 
 public class ContactListFragment extends BaseFragment implements EventListener {
 
@@ -230,12 +231,13 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 		} else if (e instanceof ContactRemovedEvent) {
 			LOG.info("Contact removed");
 			removeItem(((ContactRemovedEvent) e).getContactId());
-		} else if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
+		} else if (e instanceof MessageStateChangedEvent) {
+			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
 			ClientId c = m.getClientId();
-			if (m.isValid() && (c.equals(messagingManager.getClientId()) ||
-					c.equals(introductionManager.getClientId()) ||
-					c.equals(forumSharingManager.getClientId()))) {
+			if (m.getState() == DELIVERED &&
+					(c.equals(messagingManager.getClientId()) ||
+							c.equals(introductionManager.getClientId()) ||
+							c.equals(forumSharingManager.getClientId()))) {
 				LOG.info("Message added, reloading");
 				reloadConversation(m.getMessage().getGroupId());
 			}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index 2c8e3b5df4eda5a4c2d4d453f7da53b8904ae3be..f91d0644051705fffc3c8ce9e05f92de695e5d0a 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -47,7 +47,7 @@ import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.ForumInvitationReceivedEvent;
 import org.briarproject.api.event.IntroductionRequestReceivedEvent;
 import org.briarproject.api.event.IntroductionResponseReceivedEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
 import org.briarproject.api.forum.ForumInvitationMessage;
@@ -87,6 +87,7 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.contact.ConversationItem.IncomingItem;
 import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 
 public class ConversationActivity extends BriarActivity
 		implements EventListener, OnClickListener,
@@ -468,9 +469,10 @@ public class ConversationActivity extends BriarActivity
 				LOG.info("Contact removed");
 				finishOnUiThread();
 			}
-		} else if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
-			if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
+		} else if (e instanceof MessageStateChangedEvent) {
+			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
+			if (m.getState() == DELIVERED &&
+					m.getMessage().getGroupId().equals(groupId)) {
 				LOG.info("Message added, reloading");
 				// Mark new incoming messages as read directly
 				if (m.isLocal()) loadMessages();
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index b2069a6f2d7d94bf04e1b234d9c2049ced398fbc..cfa26e8c8adf0b7e0fd68a6ec1ed4cbb4a185eba 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -30,7 +30,7 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostHeader;
@@ -62,6 +62,7 @@ import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.forum.ReadForumPostActivity.RESULT_PREV_NEXT;
 import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
 import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 
 public class ForumActivity extends BriarActivity implements EventListener,
 		OnItemClickListener {
@@ -356,9 +357,10 @@ public class ForumActivity extends BriarActivity implements EventListener,
 	}
 
 	public void eventOccurred(Event e) {
-		if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
-			if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
+		if (e instanceof MessageStateChangedEvent) {
+			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
+			if (m.getState() == DELIVERED &&
+					m.getMessage().getGroupId().equals(groupId)) {
 				LOG.info("Message added, reloading");
 				loadHeaders();
 			}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
index e6f0f6a26685e7feee22c25e8be0a15a322dbe2a..9c32079838aa9290bf4c9b19412fc9b67affbcd9 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
@@ -25,7 +25,7 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.ForumInvitationReceivedEvent;
 import org.briarproject.api.event.GroupAddedEvent;
 import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostHeader;
@@ -42,6 +42,7 @@ import javax.inject.Inject;
 import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 
 public class ForumListFragment extends BaseEventFragment implements
 		View.OnClickListener {
@@ -224,14 +225,13 @@ public class ForumListFragment extends BaseEventFragment implements
 				LOG.info("Forum removed, removing from list");
 				removeForum(g.getGroup().getId());
 			}
-		} else if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
-			if (m.isValid()) {
-				ClientId c = m.getClientId();
-				if (c.equals(forumManager.getClientId())) {
-					LOG.info("Forum post added, reloading");
-					loadForumHeaders(m.getMessage().getGroupId());
-				}
+		} else if (e instanceof MessageStateChangedEvent) {
+			MessageStateChangedEvent m = (MessageStateChangedEvent) e;
+			ClientId c = m.getClientId();
+			if (m.getState() == DELIVERED &&
+					c.equals(forumManager.getClientId())) {
+				LOG.info("Forum post added, reloading");
+				loadForumHeaders(m.getMessage().getGroupId());
 			}
 		} else if (e instanceof ForumInvitationReceivedEvent) {
 			loadAvailableForums();
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index e145d9e8442fd022e6ee1424429950231a658cf0..123d8e4b16ae98100ca856778314eacfdc4c98de 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -22,6 +22,8 @@ import org.briarproject.api.transport.TransportKeys;
 import java.util.Collection;
 import java.util.Map;
 
+import static org.briarproject.api.sync.ValidationManager.State;
+
 /**
  * Encapsulates the database implementation and exposes high-level operations
  * to other components.
@@ -233,6 +235,24 @@ public interface DatabaseComponent {
 	Collection<MessageId> getMessagesToValidate(Transaction txn, ClientId c)
 			throws DbException;
 
+	/**
+	 * Returns the IDs of any messages that need to be delivered to the given
+	 * client.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<MessageId> getMessagesToDeliver(Transaction txn, ClientId c)
+			throws DbException;
+
+	/**
+	 * Returns the IDs of any messages that are still pending due to
+	 * dependencies to other messages for the given client.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<MessageId> getPendingMessages(Transaction txn, ClientId c)
+			throws DbException;
+
 	/**
 	 * Returns the message with the given ID, in serialised form, or null if
 	 * the message has been deleted.
@@ -276,6 +296,22 @@ public interface DatabaseComponent {
 	Collection<MessageStatus> getMessageStatus(Transaction txn, ContactId c,
 			GroupId g) throws DbException;
 
+	/**
+	 * Returns the dependencies of the given message.
+	 * <p/>
+	 * Read-only.
+	 */
+	Map<MessageId, State> getMessageDependencies(Transaction txn, MessageId m)
+			throws DbException;
+
+	/**
+	 * Returns all IDs of messages that depend on the given message.
+	 * <p/>
+	 * Read-only.
+	 */
+	Map<MessageId, State> getMessageDependents(Transaction txn, MessageId m)
+			throws DbException;
+
 	/**
 	 * Returns the status of the given message with respect to the given
 	 * contact.
@@ -391,11 +427,17 @@ public interface DatabaseComponent {
 			throws DbException;
 
 	/**
-	 * Marks the given message as valid or invalid.
+	 * Sets the state of the message with respect to validation and delivery.
 	 */
-	void setMessageValid(Transaction txn, Message m, ClientId c, boolean valid)
+	void setMessageState(Transaction txn, Message m, ClientId c, State valid)
 			throws DbException;
 
+	/**
+	 * Adds dependencies for a message
+	 */
+	void addMessageDependencies(Transaction txn, Message dependent,
+			Collection<MessageId> dependencies) throws DbException;
+
 	/**
 	 * Sets the reordering window for the given contact and transport in the
 	 * given rotation period.
diff --git a/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2aafa0347525bb9160aa064e3ea8dc9a56d7e6a
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java
@@ -0,0 +1,42 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.ValidationManager;
+import static org.briarproject.api.sync.ValidationManager.State;
+
+/**
+ * An event that is broadcast when a message state changed.
+ */
+public class MessageStateChangedEvent extends Event {
+
+	private final Message message;
+	private final ClientId clientId;
+	private final boolean local;
+	private final State state;
+
+	public MessageStateChangedEvent(Message message, ClientId clientId,
+			boolean local, State state) {
+		this.message = message;
+		this.clientId = clientId;
+		this.local = local;
+		this.state = state;
+	}
+
+	public Message getMessage() {
+		return message;
+	}
+
+	public ClientId getClientId() {
+		return clientId;
+	}
+
+	public boolean isLocal() {
+		return local;
+	}
+
+	public State getState() {
+		return state;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java
deleted file mode 100644
index 26216a873019ec9c6a1fcb5407fe06e718dd913f..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.briarproject.api.event;
-
-import org.briarproject.api.db.Metadata;
-import org.briarproject.api.sync.ClientId;
-import org.briarproject.api.sync.Message;
-
-/**
- * An event that is broadcast when a message has passed or failed validation.
- */
-public class MessageValidatedEvent extends Event {
-
-	private final Message message;
-	private final ClientId clientId;
-	private final boolean local, valid;
-
-	public MessageValidatedEvent(Message message, ClientId clientId,
-			boolean local, boolean valid) {
-		this.message = message;
-		this.clientId = clientId;
-		this.local = local;
-		this.valid = valid;
-	}
-
-	public Message getMessage() {
-		return message;
-	}
-
-	public ClientId getClientId() {
-		return clientId;
-	}
-
-	public boolean isLocal() {
-		return local;
-	}
-
-	public boolean isValid() {
-		return valid;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
index 7304b2e038254426500eb2110889ec47f5a0bb5b..e92a6a6827f1fa4dc92df217a7863c1cd3225665 100644
--- a/briar-api/src/org/briarproject/api/sync/ValidationManager.java
+++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
@@ -10,13 +10,13 @@ import org.briarproject.api.db.Transaction;
  */
 public interface ValidationManager {
 
-	enum Validity {
+	enum State {
 
-		UNKNOWN(0), INVALID(1), VALID(2);
+		UNKNOWN(0), INVALID(1), PENDING(2), VALID(3), DELIVERED(4);
 
 		private final int value;
 
-		Validity(int value) {
+		State(int value) {
 			this.value = value;
 		}
 
@@ -24,8 +24,8 @@ public interface ValidationManager {
 			return value;
 		}
 
-		public static Validity fromValue(int value) {
-			for (Validity s : values()) if (s.value == value) return s;
+		public static State fromValue(int value) {
+			for (State s : values()) if (s.value == value) return s;
 			throw new IllegalArgumentException();
 		}
 	}
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index 6589fbc81477147535e4af61402ae8944b2d8963..9779217904ba814b9aaa7deefb05edd6171300b8 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -16,7 +16,7 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
-import org.briarproject.api.sync.ValidationManager.Validity;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.api.transport.TransportKeys;
 
 import java.util.Collection;
@@ -79,9 +79,15 @@ interface Database<T> {
 	/**
 	 * Stores a message.
 	 */
-	void addMessage(T txn, Message m, Validity validity, boolean shared)
+	void addMessage(T txn, Message m, State state, boolean shared)
 			throws DbException;
 
+	/**
+	 * Adds a dependency between two MessageIds
+	 */
+	void addMessageDependency(T txn, MessageId dependentId,
+			MessageId dependencyId) throws DbException;
+
 	/**
 	 * Records that a message has been offered by the given contact.
 	 */
@@ -266,6 +272,26 @@ interface Database<T> {
 	 */
 	Collection<LocalAuthor> getLocalAuthors(T txn) throws DbException;
 
+	/**
+	 * Returns the dependencies of the given message.
+	 * This method makes sure that dependencies in different groups
+	 * are returned as {@link ValidationManager.State.INVALID}. Note that this
+	 * is not set on the dependencies themselves; the returned states should
+	 * only be taken in the context of the given message.
+	 * <p/>
+	 * Read-only.
+	 */
+	Map<MessageId, State> getMessageDependencies(T txn, MessageId m)
+			throws DbException;
+
+	/**
+	 * Returns all IDs of messages that depend on the given message.
+	 * <p/>
+	 * Read-only.
+	 */
+	Map<MessageId, State> getMessageDependents(T txn, MessageId m)
+			throws DbException;
+
 	/**
 	 * Returns the IDs of all messages in the given group.
 	 * <p/>
@@ -335,6 +361,15 @@ interface Database<T> {
 	Collection<MessageId> getMessagesToAck(T txn, ContactId c, int maxMessages)
 			throws DbException;
 
+	/**
+	 * Returns the IDs of any messages that need to be delivered to the given
+	 * client.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<MessageId> getMessagesToDeliver(T txn, ClientId c)
+			throws DbException;
+
 	/**
 	 * Returns the IDs of some messages that are eligible to be offered to the
 	 * given contact, up to the given number of messages.
@@ -344,6 +379,15 @@ interface Database<T> {
 	Collection<MessageId> getMessagesToOffer(T txn, ContactId c,
 			int maxMessages) throws DbException;
 
+	/**
+	 * Returns the IDs of some messages that are eligible to be requested from
+	 * the given contact, up to the given number of messages.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<MessageId> getMessagesToRequest(T txn, ContactId c,
+			int maxMessages) throws DbException;
+
 	/**
 	 * Returns the IDs of some messages that are eligible to be sent to the
 	 * given contact, up to the given total length.
@@ -354,21 +398,21 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the IDs of some messages that are eligible to be requested from
-	 * the given contact, up to the given number of messages.
+	 * Returns the IDs of any messages that need to be validated by the given
+	 * client.
 	 * <p/>
 	 * Read-only.
 	 */
-	Collection<MessageId> getMessagesToRequest(T txn, ContactId c,
-			int maxMessages) throws DbException;
+	Collection<MessageId> getMessagesToValidate(T txn, ClientId c)
+			throws DbException;
 
 	/**
-	 * Returns the IDs of any messages that need to be validated by the given
-	 * client.
+	 * Returns the IDs of any messages that are still pending due to
+	 * dependencies to other messages for the given client.
 	 * <p/>
 	 * Read-only.
 	 */
-	Collection<MessageId> getMessagesToValidate(T txn, ClientId c)
+	Collection<MessageId> getPendingMessages(T txn, ClientId c)
 			throws DbException;
 
 	/**
@@ -432,14 +476,14 @@ interface Database<T> {
 	void lowerRequestedFlag(T txn, ContactId c, Collection<MessageId> requested)
 			throws DbException;
 
-	/*
+	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * group.
 	 */
 	void mergeGroupMetadata(T txn, GroupId g, Metadata meta)
 			throws DbException;
 
-	/*
+	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * message.
 	 */
@@ -538,7 +582,8 @@ interface Database<T> {
 	/**
 	 * Marks the given message as valid or invalid.
 	 */
-	void setMessageValid(T txn, MessageId m, boolean valid) throws DbException;
+	void setMessageState(T txn, MessageId m, State state)
+			throws DbException;
 
 	/**
 	 * Sets the reordering window for the given contact and transport in the
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 37c1f08849656f65599113db5264d502f1171928..4f84a04bd047e0a1054743cd7331a8e90ab7c3a0 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -29,8 +29,8 @@ import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
 import org.briarproject.api.event.MessageToAckEvent;
 import org.briarproject.api.event.MessageToRequestEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
 import org.briarproject.api.identity.Author;
@@ -47,7 +47,7 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.Request;
-import org.briarproject.api.sync.ValidationManager.Validity;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.api.transport.TransportKeys;
 
 import java.util.ArrayList;
@@ -64,8 +64,8 @@ import java.util.logging.Logger;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
-import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 
 class DatabaseComponentImpl<T> implements DatabaseComponent {
@@ -193,17 +193,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		if (!db.containsGroup(txn, m.getGroupId()))
 			throw new NoSuchGroupException();
 		if (!db.containsMessage(txn, m.getId())) {
-			addMessage(txn, m, VALID, shared);
+			addMessage(txn, m, DELIVERED, shared);
 			transaction.attach(new MessageAddedEvent(m, null));
-			transaction.attach(new MessageValidatedEvent(m, c, true, true));
+			transaction.attach(new MessageStateChangedEvent(m, c, true,
+					DELIVERED));
 			if (shared) transaction.attach(new MessageSharedEvent(m));
 		}
 		db.mergeMessageMetadata(txn, m.getId(), meta);
 	}
 
-	private void addMessage(T txn, Message m, Validity validity, boolean shared)
+	private void addMessage(T txn, Message m, State state, boolean shared)
 			throws DbException {
-		db.addMessage(txn, m, validity, shared);
+		db.addMessage(txn, m, state, shared);
 		for (ContactId c : db.getVisibility(txn, m.getGroupId())) {
 			boolean offered = db.removeOfferedMessage(txn, c, m.getId());
 			db.addStatus(txn, c, m.getId(), offered, offered);
@@ -411,6 +412,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return db.getMessagesToValidate(txn, c);
 	}
 
+	public Collection<MessageId> getMessagesToDeliver(Transaction transaction,
+			ClientId c) throws DbException {
+		T txn = unbox(transaction);
+		return db.getMessagesToDeliver(txn, c);
+	}
+
+	public Collection<MessageId> getPendingMessages(Transaction transaction,
+			ClientId c) throws DbException {
+		T txn = unbox(transaction);
+		return db.getPendingMessages(txn, c);
+	}
+
 	public byte[] getRawMessage(Transaction transaction, MessageId m)
 			throws DbException {
 		T txn = unbox(transaction);
@@ -463,6 +476,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return db.getMessageStatus(txn, c, m);
 	}
 
+	public Map<MessageId, State> getMessageDependencies(Transaction transaction,
+			MessageId m) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		return db.getMessageDependencies(txn, m);
+	}
+
+	public Map<MessageId, State> getMessageDependents(Transaction transaction,
+			MessageId m) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, m))
+			throw new NoSuchMessageException();
+		return db.getMessageDependents(txn, m);
+	}
+
 	public Settings getSettings(Transaction transaction, String namespace)
 			throws DbException {
 		T txn = unbox(transaction);
@@ -664,14 +693,26 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		if (shared) transaction.attach(new MessageSharedEvent(m));
 	}
 
-	public void setMessageValid(Transaction transaction, Message m, ClientId c,
-			boolean valid) throws DbException {
+	public void setMessageState(Transaction transaction, Message m, ClientId c,
+			State state) throws DbException {
 		if (transaction.isReadOnly()) throw new IllegalArgumentException();
 		T txn = unbox(transaction);
 		if (!db.containsMessage(txn, m.getId()))
 			throw new NoSuchMessageException();
-		db.setMessageValid(txn, m.getId(), valid);
-		transaction.attach(new MessageValidatedEvent(m, c, false, valid));
+		db.setMessageState(txn, m.getId(), state);
+		transaction.attach(new MessageStateChangedEvent(m, c, false, state));
+	}
+
+	public void addMessageDependencies(Transaction transaction,
+			Message dependent, Collection<MessageId> dependencies)
+			throws DbException {
+		if (transaction.isReadOnly()) throw new IllegalArgumentException();
+		T txn = unbox(transaction);
+		if (!db.containsMessage(txn, dependent.getId()))
+			throw new NoSuchMessageException();
+		for (MessageId dependencyId : dependencies) {
+			db.addMessageDependency(txn, dependent.getId(), dependencyId);
+		}
 	}
 
 	public void setReorderingWindow(Transaction transaction, ContactId c,
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index e3e026f221b757029d6542927878bc8ed52e213c..a52fbe2a1da6c9729518c91fbb7e61cd5f39ddca 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -19,7 +19,7 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
-import org.briarproject.api.sync.ValidationManager.Validity;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.transport.IncomingKeys;
 import org.briarproject.api.transport.OutgoingKeys;
@@ -49,9 +49,11 @@ import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.db.Metadata.REMOVE;
-import static org.briarproject.api.sync.ValidationManager.Validity.INVALID;
-import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
-import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
 import static org.briarproject.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
 import static org.briarproject.db.DatabaseConstants.DEVICE_ID_KEY;
 import static org.briarproject.db.DatabaseConstants.DEVICE_SETTINGS_NAMESPACE;
@@ -65,8 +67,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 24;
-	private static final int MIN_SCHEMA_VERSION = 24;
+	private static final int SCHEMA_VERSION = 25;
+	private static final int MIN_SCHEMA_VERSION = 25;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -131,7 +133,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " (messageId HASH NOT NULL,"
 					+ " groupId HASH NOT NULL,"
 					+ " timestamp BIGINT NOT NULL,"
-					+ " valid INT NOT NULL,"
+					+ " state INT NOT NULL,"
 					+ " shared BOOLEAN NOT NULL,"
 					+ " length INT NOT NULL,"
 					+ " raw BLOB," // Null if message has been deleted
@@ -150,6 +152,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " REFERENCES messages (messageId)"
 					+ " ON DELETE CASCADE)";
 
+	private static final String CREATE_MESSAGE_DEPENDENCIES =
+			"CREATE TABLE messageDependencies"
+					+ " (messageId HASH NOT NULL,"
+					+ " dependencyId HASH NOT NULL," // Not a foreign key
+					+ " FOREIGN KEY (messageId)"
+					+ " REFERENCES messages (messageId)"
+					+ " ON DELETE CASCADE)";
+
 	private static final String CREATE_OFFERS =
 			"CREATE TABLE offers"
 					+ " (messageId HASH NOT NULL," // Not a foreign key
@@ -320,6 +330,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES));
 			s.executeUpdate(insertTypeNames(CREATE_MESSAGES));
 			s.executeUpdate(insertTypeNames(CREATE_MESSAGE_METADATA));
+			s.executeUpdate(insertTypeNames(CREATE_MESSAGE_DEPENDENCIES));
 			s.executeUpdate(insertTypeNames(CREATE_OFFERS));
 			s.executeUpdate(insertTypeNames(CREATE_STATUSES));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS));
@@ -520,18 +531,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addMessage(Connection txn, Message m, Validity validity,
+	public void addMessage(Connection txn, Message m, State state,
 			boolean shared) throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
-					+ " valid, shared, length, raw)"
+					+ " state, shared, length, raw)"
 					+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getId().getBytes());
 			ps.setBytes(2, m.getGroupId().getBytes());
 			ps.setLong(3, m.getTimestamp());
-			ps.setInt(4, validity.getValue());
+			ps.setInt(4, state.getValue());
 			ps.setBoolean(5, shared);
 			byte[] raw = m.getRaw();
 			ps.setInt(6, raw.length);
@@ -596,6 +607,25 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void addMessageDependency(Connection txn, MessageId dependentId,
+			MessageId dependencyId) throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql =
+					"INSERT INTO messageDependencies (messageId, dependencyId)"
+							+ " VALUES (?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, dependentId.getBytes());
+			ps.setBytes(2, dependencyId.getBytes());
+			int affected = ps.executeUpdate();
+			if (affected != 1) throw new DbStateException();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void addTransport(Connection txn, TransportId t, int maxLatency)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1135,10 +1165,33 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	private Collection<MessageId> getMessageIds(Connection txn, GroupId g,
+			State state) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT messageId FROM messages"
+					+ " WHERE state = ? AND groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, state.getValue());
+			ps.setBytes(2, g.getBytes());
+			rs = ps.executeQuery();
+			List<MessageId> ids = new ArrayList<MessageId>();
+			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableList(ids);
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<MessageId> getMessageIds(Connection txn, GroupId g,
 			Metadata query) throws DbException {
-		// If there are no query terms, return all messages
-		if (query.isEmpty()) return getMessageIds(txn, g);
+		// If there are no query terms, return all delivered messages
+		if (query.isEmpty()) return getMessageIds(txn, g, DELIVERED);
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1148,12 +1201,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " FROM messages AS m"
 					+ " JOIN messageMetadata AS md"
 					+ " ON m.messageId = md.messageId"
-					+ " WHERE groupId = ? AND key = ? AND value = ?";
+					+ " WHERE state = ? AND groupId = ?"
+					+ " AND key = ? AND value = ?";
 			for (Entry<String, byte[]> e : query.entrySet()) {
 				ps = txn.prepareStatement(sql);
-				ps.setBytes(1, g.getBytes());
-				ps.setString(2, e.getKey());
-				ps.setBytes(3, e.getValue());
+				ps.setInt(1, DELIVERED.getValue());
+				ps.setBytes(2, g.getBytes());
+				ps.setString(3, e.getKey());
+				ps.setBytes(4, e.getValue());
 				rs = ps.executeQuery();
 				Set<MessageId> ids = new HashSet<MessageId>();
 				while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
@@ -1182,10 +1237,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " FROM messages AS m"
 					+ " JOIN messageMetadata AS md"
 					+ " ON m.messageId = md.messageId"
-					+ " WHERE groupId = ?"
+					+ " WHERE state = ? AND groupId = ?"
 					+ " ORDER BY m.messageId";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
+			ps.setInt(1, DELIVERED.getValue());
+			ps.setBytes(2, g.getBytes());
 			rs = ps.executeQuery();
 			Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>();
 			Metadata metadata = null;
@@ -1223,23 +1279,38 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public Metadata getGroupMetadata(Connection txn, GroupId g)
 			throws DbException {
-		return getMetadata(txn, g.getBytes(), "groupMetadata", "groupId");
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT key, value FROM groupMetadata"
+					+ " WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			Metadata metadata = new Metadata();
+			while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2));
+			rs.close();
+			ps.close();
+			return metadata;
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
 	}
 
 	public Metadata getMessageMetadata(Connection txn, MessageId m)
 			throws DbException {
-		return getMetadata(txn, m.getBytes(), "messageMetadata", "messageId");
-	}
-
-	private Metadata getMetadata(Connection txn, byte[] id, String tableName,
-			String columnName) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT key, value FROM " + tableName
-					+ " WHERE " + columnName + " = ?";
+			String sql = "SELECT key, value FROM messageMetadata AS md"
+					+ " JOIN messages AS m"
+					+ " ON m.messageId = md.messageId"
+					+ " WHERE m.state = ? AND md.messageId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, id);
+			ps.setInt(1, DELIVERED.getValue());
+			ps.setBytes(2, m.getBytes());
 			rs = ps.executeQuery();
 			Metadata metadata = new Metadata();
 			while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2));
@@ -1312,6 +1383,91 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Map<MessageId, State> getMessageDependencies(Connection txn,
+			MessageId m) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT d.dependencyId, m.state, m.groupId"
+					+ " FROM messageDependencies AS d"
+					+ " LEFT OUTER JOIN messages AS m"
+					+ " ON d.dependencyId = m.messageId"
+					+ " WHERE d.messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			rs = ps.executeQuery();
+			Map<MessageId, State> dependencies = new HashMap<MessageId, State>();
+			while (rs.next()) {
+				MessageId messageId = new MessageId(rs.getBytes(1));
+				State state = State.fromValue(rs.getInt(2));
+				if (state != UNKNOWN) {
+					// set dependency invalid if it is in a different group
+					if (!hasGroupId(txn, m, rs.getBytes(3))) state = INVALID;
+				}
+				dependencies.put(messageId, state);
+			}
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableMap(dependencies);
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	private boolean hasGroupId(Connection txn, MessageId m, byte[] g)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT NULL FROM messages"
+					+ " WHERE messageId = ? AND groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			ps.setBytes(2, g);
+			rs = ps.executeQuery();
+			boolean same = rs.next();
+			if (rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return same;
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public Map<MessageId, State> getMessageDependents(Connection txn,
+			MessageId m) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT d.messageId, m.state"
+					+ " FROM messageDependencies AS d"
+					+ " LEFT OUTER JOIN messages AS m"
+					+ " ON d.messageId = m.messageId"
+					+ " WHERE dependencyId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			rs = ps.executeQuery();
+			Map<MessageId, State> dependents = new HashMap<MessageId, State>();
+			while (rs.next()) {
+				MessageId messageId = new MessageId(rs.getBytes(1));
+				State state = State.fromValue(rs.getInt(2));
+				dependents.put(messageId, state);
+			}
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableMap(dependents);
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c,
 			int maxMessages) throws DbException {
 		PreparedStatement ps = null;
@@ -1346,13 +1502,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
 					+ " WHERE contactId = ?"
-					+ " AND valid = ? AND shared = TRUE AND raw IS NOT NULL"
+					+ " AND state = ? AND shared = TRUE AND raw IS NOT NULL"
 					+ " AND seen = FALSE AND requested = FALSE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp DESC LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, VALID.getValue());
+			ps.setInt(2, DELIVERED.getValue());
 			ps.setLong(3, now);
 			ps.setInt(4, maxMessages);
 			rs = ps.executeQuery();
@@ -1402,13 +1558,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
 					+ " WHERE contactId = ?"
-					+ " AND valid = ? AND shared = TRUE AND raw IS NOT NULL"
+					+ " AND state = ? AND shared = TRUE AND raw IS NOT NULL"
 					+ " AND seen = FALSE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp DESC";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, VALID.getValue());
+			ps.setInt(2, DELIVERED.getValue());
 			ps.setLong(3, now);
 			rs = ps.executeQuery();
 			List<MessageId> ids = new ArrayList<MessageId>();
@@ -1431,14 +1587,29 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public Collection<MessageId> getMessagesToValidate(Connection txn,
 			ClientId c) throws DbException {
+		return getMessagesInState(txn, c, UNKNOWN);
+	}
+
+	public Collection<MessageId> getMessagesToDeliver(Connection txn,
+			ClientId c) throws DbException {
+		return getMessagesInState(txn, c, VALID);
+	}
+
+	public Collection<MessageId> getPendingMessages(Connection txn,
+			ClientId c) throws DbException {
+		return getMessagesInState(txn, c, PENDING);
+	}
+
+	private Collection<MessageId> getMessagesInState(Connection txn, ClientId c,
+			State state) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT messageId FROM messages AS m"
 					+ " JOIN groups AS g ON m.groupId = g.groupId"
-					+ " WHERE valid = ? AND clientId = ? AND raw IS NOT NULL";
+					+ " WHERE state = ? AND clientId = ? AND raw IS NOT NULL";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, UNKNOWN.getValue());
+			ps.setInt(1, state.getValue());
 			ps.setBytes(2, c.getBytes());
 			rs = ps.executeQuery();
 			List<MessageId> ids = new ArrayList<MessageId>();
@@ -1485,13 +1656,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
 					+ " WHERE contactId = ?"
-					+ " AND valid = ? AND shared = TRUE AND raw IS NOT NULL"
+					+ " AND state = ? AND shared = TRUE AND raw IS NOT NULL"
 					+ " AND seen = FALSE AND requested = TRUE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp DESC";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, VALID.getValue());
+			ps.setInt(2, DELIVERED.getValue());
 			ps.setLong(3, now);
 			rs = ps.executeQuery();
 			List<MessageId> ids = new ArrayList<MessageId>();
@@ -2081,13 +2252,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setMessageValid(Connection txn, MessageId m, boolean valid)
+	public void setMessageState(Connection txn, MessageId m, State state)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "UPDATE messages SET valid = ? WHERE messageId = ?";
+			String sql = "UPDATE messages SET state = ? WHERE messageId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, valid ? VALID.getValue() : INVALID.getValue());
+			ps.setInt(1, state.getValue());
 			ps.setBytes(2, m.getBytes());
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
index 6dbc68e96f6ff5ff8a8134d1b663ba5ea5f29ed8..e0a86a7b8c14a18560971f50477a8cc4cdbc2d4f 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
@@ -2,8 +2,8 @@ package org.briarproject.forum;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.UniqueId;
-import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.BdfMessageContext;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyParser;
 import org.briarproject.api.crypto.PublicKey;
@@ -16,10 +16,13 @@ import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.system.Clock;
 import org.briarproject.clients.BdfMessageValidator;
 
 import java.security.GeneralSecurityException;
+import java.util.Collection;
+import java.util.Collections;
 
 import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
@@ -96,10 +99,14 @@ class ForumPostValidator extends BdfMessageValidator {
 				throw new InvalidMessageException("Invalid public key");
 			}
 		}
-		// Return the metadata
+		// Return the metadata and dependencies
 		BdfDictionary meta = new BdfDictionary();
+		Collection<MessageId> dependencies = null;
 		meta.put("timestamp", m.getTimestamp());
-		if (parent != null) meta.put("parent", parent);
+		if (parent != null) {
+			meta.put("parent", parent);
+			dependencies = Collections.singletonList(new MessageId(parent));
+		}
 		if (author != null) {
 			BdfDictionary authorMeta = new BdfDictionary();
 			authorMeta.put("id", author.getId());
@@ -109,6 +116,6 @@ class ForumPostValidator extends BdfMessageValidator {
 		}
 		meta.put("contentType", contentType);
 		meta.put("read", false);
-		return new BdfMessageContext(meta);
+		return new BdfMessageContext(meta, dependencies);
 	}
 }
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 1e85803a22aba9d836f5564b23b3fb8854f10917..f6aa74c3ab07fcd7bbb0ba5f211f027270980329 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -23,8 +23,10 @@ import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.api.sync.MessageContext;
 import org.briarproject.util.ByteUtils;
 
+import java.util.Collection;
 import java.util.LinkedList;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
@@ -36,6 +38,10 @@ import javax.inject.Inject;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
 
 class ValidationManagerImpl implements ValidationManager, Service,
 		EventListener {
@@ -64,7 +70,10 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	@Override
 	public void startService() {
 		if (used.getAndSet(true)) throw new IllegalStateException();
-		for (ClientId c : validators.keySet()) getMessagesToValidate(c);
+		for (ClientId c : validators.keySet()) {
+			validateOutstandingMessages(c);
+			deliverOutstandingMessages(c);
+		}
 	}
 
 	@Override
@@ -82,7 +91,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		hooks.put(c, hook);
 	}
 
-	private void getMessagesToValidate(final ClientId c) {
+	private void validateOutstandingMessages(final ClientId c) {
 		dbExecutor.execute(new Runnable() {
 			@Override
 			public void run() {
@@ -126,6 +135,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 						LOG.info("Group removed before validation");
 						// Continue to next message
 					} finally {
+						if (!txn.isComplete()) txn.setComplete();
 						db.endTransaction(txn);
 					}
 					if (m != null && g != null) validateMessage(m, g);
@@ -138,6 +148,112 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		});
 	}
 
+	private void deliverOutstandingMessages(final ClientId c) {
+		dbExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Queue<MessageId> validated = new LinkedList<MessageId>();
+					Queue<MessageId> pending = new LinkedList<MessageId>();
+					Transaction txn = db.startTransaction(true);
+					try {
+						validated.addAll(db.getMessagesToDeliver(txn, c));
+						pending.addAll(db.getPendingMessages(txn, c));
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
+					deliverNextMessage(validated);
+					deliverNextPendingMessage(pending);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void deliverNextMessage(final Queue<MessageId> validated) {
+		if (validated.isEmpty()) return;
+		dbExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Message m = null;
+					Group g = null;
+					Metadata meta = null;
+					Transaction txn = db.startTransaction(true);
+					try {
+						MessageId id = validated.poll();
+						byte[] raw = db.getRawMessage(txn, id);
+						m = parseMessage(id, raw);
+						g = db.getGroup(txn, m.getGroupId());
+						meta = db.getMessageMetadata(txn, id);
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
+					if (g != null) deliverMessage(m, g.getClientId(), meta);
+					deliverNextMessage(validated);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void deliverNextPendingMessage(final Queue<MessageId> pending) {
+		if (pending.isEmpty()) return;
+		dbExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				Message m = null;
+				ClientId c = null;
+				try {
+					boolean allDelivered = true;
+					Metadata meta = null;
+					Transaction txn = db.startTransaction(true);
+					try {
+						MessageId id = pending.poll();
+						byte[] raw = db.getRawMessage(txn, id);
+						m = parseMessage(id, raw);
+						Group g = db.getGroup(txn, m.getGroupId());
+						c = g.getClientId();
+
+						// check if a dependency is invalid
+						Map<MessageId, State> states =
+								db.getMessageDependencies(txn, id);
+						for (Entry<MessageId, State> d : states.entrySet()) {
+							if (d.getValue() == INVALID) {
+								throw new InvalidMessageException(
+										"Invalid Dependency");
+							}
+							if (d.getValue() != DELIVERED) allDelivered = false;
+						}
+						if(allDelivered) {
+							meta = db.getMessageMetadata(txn, id);
+						}
+						txn.setComplete();
+					} finally {
+						if (!txn.isComplete()) txn.setComplete();
+						db.endTransaction(txn);
+					}
+					if (c != null && allDelivered) deliverMessage(m, c, meta);
+					deliverNextPendingMessage(pending);
+				} catch(InvalidMessageException e) {
+					if (LOG.isLoggable(INFO))
+						LOG.log(INFO, e.toString(), e);
+					markMessageInvalid(m, c);
+					deliverNextPendingMessage(pending);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
 	private Message parseMessage(MessageId id, byte[] raw) {
 		if (raw.length <= MESSAGE_HEADER_LENGTH)
 			throw new IllegalArgumentException();
@@ -174,19 +290,105 @@ class ValidationManagerImpl implements ValidationManager, Service,
 			@Override
 			public void run() {
 				try {
+					State newState = null;
+					Metadata meta = null;
 					Transaction txn = db.startTransaction(false);
 					try {
-						Metadata meta = result.getMetadata();
+						// store dependencies
+						Collection<MessageId> dependencies =
+								result.getDependencies();
+						if (dependencies != null && dependencies.size() > 0) {
+							db.addMessageDependencies(txn, m, dependencies);
+						}
+						// check if a dependency is invalid
+						// and if all dependencies have been delivered
+						Map<MessageId, State> states =
+								db.getMessageDependencies(txn, m.getId());
+						newState = VALID;
+						for (Entry<MessageId, State> d : states.entrySet()) {
+							if (d.getValue() == INVALID) {
+								throw new InvalidMessageException(
+										"Dependency Invalid");
+							}
+							if (d.getValue() != DELIVERED) {
+								newState = PENDING;
+								LOG.info("depend. undelivered, set to PENDING");
+								break;
+							}
+						}
+						// save metadata and new message state
+						meta = result.getMetadata();
 						db.mergeMessageMetadata(txn, m.getId(), meta);
-						db.setMessageValid(txn, m, c, true);
-						db.setMessageShared(txn, m, true);
+						db.setMessageState(txn, m, c, newState);
+						txn.setComplete();
+					} finally {
+						if (!txn.isComplete()) txn.setComplete();
+						db.endTransaction(txn);
+					}
+					// deliver message if valid
+					if (newState == VALID) {
+						deliverMessage(m, c, meta);
+					}
+				} catch (InvalidMessageException e) {
+					if (LOG.isLoggable(INFO))
+						LOG.log(INFO, e.toString(), e);
+					markMessageInvalid(m, c);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void deliverMessage(final Message m, final ClientId c,
+			final Metadata meta) {
+		dbExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Queue<MessageId> pending = new LinkedList<MessageId>();
+					Transaction txn = db.startTransaction(false);
+					try {
 						IncomingMessageHook hook = hooks.get(c);
 						if (hook != null)
 							hook.incomingMessage(txn, m, meta);
+
+						// check if message was deleted by client
+						if (db.getRawMessage(txn, m.getId()) == null) {
+							throw new InvalidMessageException(
+									"Deleted by Client");
+						}
+
+						db.setMessageShared(txn, m, true);
+						db.setMessageState(txn, m, c, DELIVERED);
+
+						// deliver pending dependents
+						Map<MessageId, State> dependents =
+								db.getMessageDependents(txn, m.getId());
+						for (Entry<MessageId, State> i : dependents
+								.entrySet()) {
+							if (i.getValue() != PENDING) continue;
+
+							// check that all dependencies are delivered
+							Map<MessageId, State> dependencies =
+									db.getMessageDependencies(txn, i.getKey());
+							for (Entry<MessageId, State> j : dependencies
+									.entrySet()) {
+								if (j.getValue() != DELIVERED) return;
+							}
+							pending.add(i.getKey());
+						}
 						txn.setComplete();
 					} finally {
+						if (!txn.isComplete()) txn.setComplete();
 						db.endTransaction(txn);
 					}
+					deliverNextMessage(pending);
+				} catch (InvalidMessageException e) {
+					if (LOG.isLoggable(INFO))
+						LOG.log(INFO, e.toString(), e);
+					markMessageInvalid(m, c);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -200,13 +402,56 @@ class ValidationManagerImpl implements ValidationManager, Service,
 			@Override
 			public void run() {
 				try {
+					Queue<MessageId> invalid = new LinkedList<MessageId>();
 					Transaction txn = db.startTransaction(false);
 					try {
-						db.setMessageValid(txn, m, c, false);
+						Map<MessageId, State> dependents =
+								db.getMessageDependents(txn, m.getId());
+						db.setMessageState(txn, m, c, INVALID);
+						db.deleteMessage(txn, m.getId());
+						db.deleteMessageMetadata(txn, m.getId());
+
+						// recursively invalidate all messages that depend on m
+						// TODO check that cycles are properly taken care of
+						for (Entry<MessageId, State> i : dependents
+								.entrySet()) {
+							if (i.getValue() != INVALID) {
+								invalid.add(i.getKey());
+							}
+						}
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
+					markNextMessageInvalid(invalid);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void markNextMessageInvalid(final Queue<MessageId> invalid) {
+		if (invalid.isEmpty()) return;
+		dbExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Message m = null;
+					Group g = null;
+					Transaction txn = db.startTransaction(true);
+					try {
+						MessageId id = invalid.poll();
+						byte[] raw = db.getRawMessage(txn, id);
+						m = parseMessage(id, raw);
+						g = db.getGroup(txn, m.getGroupId());
 						txn.setComplete();
 					} finally {
 						db.endTransaction(txn);
 					}
+					if (g != null) markMessageInvalid(m, g.getClientId());
+					markNextMessageInvalid(invalid);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index ff6f7a918cc75941551be6c222aa64bade8358c5..6a4db39e815c184f835145d8a325832f45d5831e 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -29,7 +29,7 @@ import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
 import org.briarproject.api.event.MessageToAckEvent;
 import org.briarproject.api.event.MessageToRequestEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.event.MessageStateChangedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
@@ -53,6 +53,7 @@ import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -60,8 +61,9 @@ import java.util.Map;
 
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
-import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
-import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
 import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 import static org.junit.Assert.assertEquals;
@@ -264,7 +266,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(false));
-			oneOf(database).addMessage(txn, message, VALID, true);
+			oneOf(database).addMessage(txn, message, DELIVERED, true);
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
@@ -274,7 +276,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).commitTransaction(txn);
 			// The message was added, so the listeners should be called
 			oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
-			oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class)));
+			oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -675,11 +677,11 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
 			// Check whether the message is in the DB (which it's not)
-			exactly(8).of(database).startTransaction();
+			exactly(10).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(8).of(database).containsMessage(txn, messageId);
+			exactly(10).of(database).containsMessage(txn, messageId);
 			will(returnValue(false));
-			exactly(8).of(database).abortTransaction(txn);
+			exactly(10).of(database).abortTransaction(txn);
 			// This is needed for getMessageStatus() to proceed
 			exactly(1).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
@@ -759,7 +761,27 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			db.setMessageValid(transaction, message, clientId, true);
+			db.setMessageState(transaction, message, clientId, VALID);
+			fail();
+		} catch (NoSuchMessageException expected) {
+			// Expected
+		} finally {
+			db.endTransaction(transaction);
+		}
+
+		transaction = db.startTransaction(true);
+		try {
+			db.getMessageDependencies(transaction, messageId);
+			fail();
+		} catch (NoSuchMessageException expected) {
+			// Expected
+		} finally {
+			db.endTransaction(transaction);
+		}
+
+		transaction = db.startTransaction(true);
+		try {
+			db.getMessageDependents(transaction, messageId);
 			fail();
 		} catch (NoSuchMessageException expected) {
 			// Expected
@@ -1595,4 +1617,79 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		context.assertIsSatisfied();
 	}
+
+	@Test
+	@SuppressWarnings("unchecked")
+	public void testMessageDependencies() throws Exception {
+		final int shutdownHandle = 12345;
+		Mockery context = new Mockery();
+		final Database<Object> database = context.mock(Database.class);
+		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
+		final EventBus eventBus = context.mock(EventBus.class);
+		final MessageId messageId2 = new MessageId(TestUtils.getRandomId());
+		context.checking(new Expectations() {{
+			// open()
+			oneOf(database).open();
+			will(returnValue(false));
+			oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
+			will(returnValue(shutdownHandle));
+			// startTransaction()
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			// addLocalMessage()
+			oneOf(database).containsGroup(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(false));
+			oneOf(database).addMessage(txn, message, DELIVERED, true);
+			oneOf(database).getVisibility(txn, groupId);
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
+			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
+			will(returnValue(false));
+			oneOf(database).addStatus(txn, contactId, messageId, false, false);
+			// addMessageDependencies()
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(true));
+			oneOf(database).addMessageDependency(txn, messageId, messageId1);
+			oneOf(database).addMessageDependency(txn, messageId, messageId2);
+			// getMessageDependencies()
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(true));
+			oneOf(database).getMessageDependencies(txn, messageId);
+			// getMessageDependents()
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(true));
+			oneOf(database).getMessageDependents(txn, messageId);
+			// broadcast for message added event
+			oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
+			oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class)));
+			oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
+			// endTransaction()
+			oneOf(database).commitTransaction(txn);
+			// close()
+			oneOf(shutdown).removeShutdownHook(shutdownHandle);
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, eventBus,
+				shutdown);
+
+		assertFalse(db.open());
+		Transaction transaction = db.startTransaction(false);
+		try {
+			db.addLocalMessage(transaction, message, clientId, metadata, true);
+			Collection<MessageId> dependencies = new ArrayList<>(2);
+			dependencies.add(messageId1);
+			dependencies.add(messageId2);
+			db.addMessageDependencies(transaction, message, dependencies);
+			db.getMessageDependencies(transaction, messageId);
+			db.getMessageDependents(transaction, messageId);
+			transaction.setComplete();
+		} finally {
+			db.endTransaction(transaction);
+		}
+		db.close();
+
+		context.assertIsSatisfied();
+	}
 }
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index d967b815973b6bb6246dc23d6075b5621522e7b7..9d5b1f0b912e9cd94d5086a31d17b546859fb1e4 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -19,6 +19,7 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.api.transport.IncomingKeys;
 import org.briarproject.api.transport.OutgoingKeys;
 import org.briarproject.api.transport.TransportKeys;
@@ -46,8 +47,11 @@ import static org.briarproject.api.db.Metadata.REMOVE;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
-import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN;
-import static org.briarproject.api.sync.ValidationManager.Validity.VALID;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -114,7 +118,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroup(txn, group);
 		assertTrue(db.containsGroup(txn, groupId));
 		assertFalse(db.containsMessage(txn, messageId));
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		assertTrue(db.containsMessage(txn, messageId));
 		db.commitTransaction(txn);
 		db.close();
@@ -152,7 +156,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 
 		// Removing the group should remove the message
 		assertTrue(db.containsMessage(txn, messageId));
@@ -174,7 +178,7 @@ public class H2DatabaseTest extends BriarTestCase {
 				true));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 
 		// The message has no status yet, so it should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -202,7 +206,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testSendableMessagesMustBeValid() throws Exception {
+	public void testSendableMessagesMustBeDelivered() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
@@ -222,15 +226,31 @@ public class H2DatabaseTest extends BriarTestCase {
 		ids = db.getMessagesToOffer(txn, contactId, 100);
 		assertTrue(ids.isEmpty());
 
-		// Marking the message valid should make it sendable
-		db.setMessageValid(txn, messageId, true);
+		// Marking the message delivered should make it sendable
+		db.setMessageState(txn, messageId, DELIVERED);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertEquals(Collections.singletonList(messageId), ids);
 		ids = db.getMessagesToOffer(txn, contactId, 100);
 		assertEquals(Collections.singletonList(messageId), ids);
 
 		// Marking the message invalid should make it unsendable
-		db.setMessageValid(txn, messageId, false);
+		db.setMessageState(txn, messageId, INVALID);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Marking the message valid should make it unsendable
+		// TODO do we maybe want to already send valid messages? If we do, we need also to call db.setMessageShared() earlier.
+		db.setMessageState(txn, messageId, VALID);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Marking the message pending should make it unsendable
+		// TODO do we maybe want to already send pending messages? If we do, we need also to call db.setMessageShared() earlier.
+		db.setMessageState(txn, messageId, PENDING);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
 		ids = db.getMessagesToOffer(txn, contactId, 100);
@@ -251,7 +271,7 @@ public class H2DatabaseTest extends BriarTestCase {
 				true));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, VALID, false);
+		db.addMessage(txn, message, DELIVERED, false);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The message is not shared, so it should not be sendable
@@ -290,7 +310,7 @@ public class H2DatabaseTest extends BriarTestCase {
 				true));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The message is sendable, but too large to send
@@ -321,10 +341,10 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add some messages to ack
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		Message message1 = new Message(messageId1, groupId, timestamp, raw);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, true);
 		db.raiseAckFlag(txn, contactId, messageId);
-		db.addMessage(txn, message1, VALID, true);
+		db.addMessage(txn, message1, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId1, false, true);
 		db.raiseAckFlag(txn, contactId, messageId1);
 
@@ -354,7 +374,7 @@ public class H2DatabaseTest extends BriarTestCase {
 				true));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// Retrieve the message from the database and mark it as sent
@@ -396,7 +416,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Storing a message should reduce the free space
 		Connection txn = db.startTransaction();
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.commitTransaction(txn);
 		assertTrue(db.getFreeSpace() < free);
 
@@ -555,7 +575,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true));
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The group is not visible
@@ -866,7 +886,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 
 		// Attach some metadata to the message
 		Metadata metadata = new Metadata();
@@ -930,6 +950,67 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testMessageMetadataOnlyForDeliveredMessages() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+
+		// Attach some metadata to the message
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+
+		// Retrieve the metadata for the message
+		Metadata retrieved = db.getMessageMetadata(txn, messageId);
+		assertEquals(2, retrieved.size());
+		assertTrue(retrieved.containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), retrieved.get("foo"));
+		assertTrue(retrieved.containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), retrieved.get("baz"));
+		Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId);
+		assertEquals(1, map.size());
+		assertTrue(map.get(messageId).containsKey("foo"));
+		assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo"));
+		assertTrue(map.get(messageId).containsKey("baz"));
+		assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz"));
+
+		// No metadata for unknown messages
+		db.setMessageState(txn, messageId, UNKNOWN);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		// No metadata for invalid messages
+		db.setMessageState(txn, messageId, INVALID);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		// No metadata for valid messages
+		db.setMessageState(txn, messageId, VALID);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		// No metadata for pending messages
+		db.setMessageState(txn, messageId, PENDING);
+		retrieved = db.getMessageMetadata(txn, messageId);
+		assertTrue(retrieved.isEmpty());
+		map = db.getMessageMetadata(txn, groupId);
+		assertTrue(map.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testMetadataQueries() throws Exception {
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
@@ -940,8 +1021,8 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Add a group and two messages
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, VALID, true);
-		db.addMessage(txn, message1, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message1, DELIVERED, true);
 
 		// Attach some metadata to the messages
 		Metadata metadata = new Metadata();
@@ -1034,6 +1115,257 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception {
+		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+		Message message1 = new Message(messageId1, groupId, timestamp, raw);
+
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and two messages
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message1, DELIVERED, true);
+
+		// Attach some metadata to the messages
+		Metadata metadata = new Metadata();
+		metadata.put("foo", new byte[]{'b', 'a', 'r'});
+		metadata.put("baz", new byte[]{'b', 'a', 'm'});
+		db.mergeMessageMetadata(txn, messageId, metadata);
+		Metadata metadata1 = new Metadata();
+		metadata1.put("foo", new byte[]{'b', 'a', 'r'});
+		db.mergeMessageMetadata(txn, messageId1, metadata1);
+
+		for (int i = 1; i <= 2; i++) {
+			Metadata query;
+			if (i == 1) {
+				// Query the metadata with an empty query
+				query = new Metadata();
+			} else {
+				// Query for foo
+				query = new Metadata();
+				query.put("foo", metadata.get("foo"));
+			}
+
+			db.setMessageState(txn, messageId, DELIVERED);
+			db.setMessageState(txn, messageId1, DELIVERED);
+			Map<MessageId, Metadata> all =
+					db.getMessageMetadata(txn, groupId, query);
+			assertEquals(2, all.size());
+			assertEquals(2, all.get(messageId).size());
+			assertEquals(1, all.get(messageId1).size());
+
+			// No metadata for unknown messages
+			db.setMessageState(txn, messageId, UNKNOWN);
+			db.setMessageState(txn, messageId1, UNKNOWN);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertTrue(all.isEmpty());
+
+			// No metadata for invalid messages
+			db.setMessageState(txn, messageId, INVALID);
+			db.setMessageState(txn, messageId1, INVALID);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertTrue(all.isEmpty());
+
+			// No metadata for valid messages
+			db.setMessageState(txn, messageId, VALID);
+			db.setMessageState(txn, messageId1, VALID);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertTrue(all.isEmpty());
+
+			// No metadata for pending messages
+			db.setMessageState(txn, messageId, PENDING);
+			db.setMessageState(txn, messageId1, PENDING);
+			all = db.getMessageMetadata(txn, groupId, query);
+			assertTrue(all.isEmpty());
+		}
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMessageDependencies() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, VALID, true);
+
+		// Create more messages
+		MessageId mId1 = new MessageId(TestUtils.getRandomId());
+		MessageId mId2 = new MessageId(TestUtils.getRandomId());
+		MessageId dId1 = new MessageId(TestUtils.getRandomId());
+		MessageId dId2 = new MessageId(TestUtils.getRandomId());
+		Message m1 = new Message(mId1, groupId, timestamp, raw);
+		Message m2 = new Message(mId2, groupId, timestamp, raw);
+
+		// Add new messages
+		db.addMessage(txn, m1, VALID, true);
+		db.addMessage(txn, m2, INVALID, true);
+
+		// Add dependencies
+		db.addMessageDependency(txn, messageId, mId1);
+		db.addMessageDependency(txn, messageId, mId2);
+		db.addMessageDependency(txn, mId1, dId1);
+		db.addMessageDependency(txn, mId2, dId2);
+
+		Map<MessageId, State> dependencies;
+
+		// Retrieve dependencies for root
+		dependencies = db.getMessageDependencies(txn, messageId);
+		assertEquals(2, dependencies.size());
+		assertEquals(VALID, dependencies.get(mId1));
+		assertEquals(INVALID, dependencies.get(mId2));
+
+		// Retrieve dependencies for m1
+		dependencies = db.getMessageDependencies(txn, mId1);
+		assertEquals(1, dependencies.size());
+		assertEquals(UNKNOWN, dependencies.get(dId1));
+
+		// Retrieve dependencies for m2
+		dependencies = db.getMessageDependencies(txn, mId2);
+		assertEquals(1, dependencies.size());
+		assertEquals(UNKNOWN, dependencies.get(dId2));
+
+		// Make sure d's have no dependencies
+		dependencies = db.getMessageDependencies(txn, dId1);
+		assertTrue(dependencies.isEmpty());
+		dependencies = db.getMessageDependencies(txn, dId2);
+		assertTrue(dependencies.isEmpty());
+
+		Map<MessageId, State> dependents;
+
+		// Root message does not have dependents
+		dependents = db.getMessageDependents(txn, messageId);
+		assertTrue(dependents.isEmpty());
+
+		// The root message depends on both m's
+		dependents = db.getMessageDependents(txn, mId1);
+		assertEquals(1, dependents.size());
+		assertEquals(VALID, dependents.get(messageId));
+		dependents = db.getMessageDependents(txn, mId2);
+		assertEquals(1, dependents.size());
+		assertEquals(VALID, dependents.get(messageId));
+
+		// Both m's depend on the d's
+		dependents = db.getMessageDependents(txn, dId1);
+		assertEquals(1, dependents.size());
+		assertEquals(VALID, dependents.get(mId1));
+		dependents = db.getMessageDependents(txn, dId2);
+		assertEquals(1, dependents.size());
+		assertEquals(INVALID, dependents.get(mId2));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testMessageDependenciesInSameGroup() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+
+		// Add a second group
+		GroupId groupId1 = new GroupId(TestUtils.getRandomId());
+		Group group1 = new Group(groupId1, group.getClientId(),
+				TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH));
+		db.addGroup(txn, group1);
+
+		// Add a message to the second group
+		MessageId mId1 = new MessageId(TestUtils.getRandomId());
+		Message m1 = new Message(mId1, groupId1, timestamp, raw);
+		db.addMessage(txn, m1, DELIVERED, true);
+
+		// Create a fake dependency as well
+		MessageId mId2 = new MessageId(TestUtils.getRandomId());
+
+		// Create and add a real and proper dependency
+		MessageId mId3 = new MessageId(TestUtils.getRandomId());
+		Message m3 = new Message(mId3, groupId, timestamp, raw);
+		db.addMessage(txn, m3, PENDING, true);
+
+		// Add dependencies
+		db.addMessageDependency(txn, messageId, mId1);
+		db.addMessageDependency(txn, messageId, mId2);
+		db.addMessageDependency(txn, messageId, mId3);
+
+		// Return invalid dependencies for delivered message m1
+		Map<MessageId, State> dependencies;
+		dependencies = db.getMessageDependencies(txn, messageId);
+		assertEquals(INVALID, dependencies.get(mId1));
+		assertEquals(UNKNOWN, dependencies.get(mId2));
+		assertEquals(PENDING, dependencies.get(mId3));
+
+		// Return invalid dependencies for valid message m1
+		db.setMessageState(txn, mId1, VALID);
+		dependencies = db.getMessageDependencies(txn, messageId);
+		assertEquals(INVALID, dependencies.get(mId1));
+		assertEquals(UNKNOWN, dependencies.get(mId2));
+		assertEquals(PENDING, dependencies.get(mId3));
+
+		// Return invalid dependencies for pending message m1
+		db.setMessageState(txn, mId1, PENDING);
+		dependencies = db.getMessageDependencies(txn, messageId);
+		assertEquals(INVALID, dependencies.get(mId1));
+		assertEquals(UNKNOWN, dependencies.get(mId2));
+		assertEquals(PENDING, dependencies.get(mId3));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testGetMessagesForValidationAndDelivery() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a group and a message
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, VALID, true);
+
+		// Create more messages
+		MessageId mId1 = new MessageId(TestUtils.getRandomId());
+		MessageId mId2 = new MessageId(TestUtils.getRandomId());
+		MessageId mId3 = new MessageId(TestUtils.getRandomId());
+		MessageId mId4 = new MessageId(TestUtils.getRandomId());
+		Message m1 = new Message(mId1, groupId, timestamp, raw);
+		Message m2 = new Message(mId2, groupId, timestamp, raw);
+		Message m3 = new Message(mId3, groupId, timestamp, raw);
+		Message m4 = new Message(mId4, groupId, timestamp, raw);
+
+		// Add new messages with different states
+		db.addMessage(txn, m1, UNKNOWN, true);
+		db.addMessage(txn, m2, INVALID, true);
+		db.addMessage(txn, m3, PENDING, true);
+		db.addMessage(txn, m4, DELIVERED, true);
+
+		Collection<MessageId> result;
+
+		// Retrieve messages to be validated
+		result = db.getMessagesToValidate(txn, group.getClientId());
+		assertEquals(1, result.size());
+		assertTrue(result.contains(mId1));
+
+		// Retrieve messages to be delivered
+		result = db.getMessagesToDeliver(txn, group.getClientId());
+		assertEquals(1, result.size());
+		assertTrue(result.contains(messageId));
+
+		// Retrieve pending messages
+		result = db.getPendingMessages(txn, group.getClientId());
+		assertEquals(1, result.size());
+		assertTrue(result.contains(mId3));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testGetMessageStatus() throws Exception {
 		Database<Connection> db = open(false);
@@ -1049,7 +1381,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addVisibility(txn, contactId, groupId);
 
 		// Add a message to the group
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The message should not be sent or seen
@@ -1176,7 +1508,7 @@ public class H2DatabaseTest extends BriarTestCase {
 				true));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, VALID, true);
+		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The message should be visible to the contact
diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
index 1aea2e30b01fea7a8db15b210a8fb1fc5a77a345..0520b3b1fffe9b0be5bf1216e8a79487663dd738 100644
--- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
@@ -16,23 +16,37 @@ import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageContext;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.ValidationManager.IncomingMessageHook;
 import org.briarproject.api.sync.ValidationManager.MessageValidator;
-import org.briarproject.api.sync.MessageContext;
+import org.briarproject.api.sync.ValidationManager.State;
 import org.briarproject.util.ByteUtils;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.PENDING;
+import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
+import static org.junit.Assert.assertTrue;
+
 public class ValidationManagerImplTest extends BriarTestCase {
 
 	private final ClientId clientId = new ClientId(TestUtils.getRandomId());
 	private final MessageId messageId = new MessageId(TestUtils.getRandomId());
 	private final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+	private final MessageId messageId2 = new MessageId(TestUtils.getRandomId());
 	private final GroupId groupId = new GroupId(TestUtils.getRandomId());
 	private final byte[] descriptor = new byte[32];
 	private final Group group = new Group(groupId, clientId, descriptor);
@@ -42,14 +56,23 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			raw);
 	private final Message message1 = new Message(messageId1, groupId, timestamp,
 			raw);
+	private final Message message2 = new Message(messageId2, groupId, timestamp,
+			raw);
 	private final Metadata metadata = new Metadata();
-	final MessageContext validResult = new MessageContext(metadata);
+	private final MessageContext validResult = new MessageContext(metadata);
 	private final ContactId contactId = new ContactId(234);
+	private final Collection<MessageId> dependencies = new ArrayList<>();
+	private final MessageContext validResultWithDependencies =
+			new MessageContext(metadata, dependencies);
+	private final Map<MessageId, State> states = new HashMap<>();
 
 	public ValidationManagerImplTest() {
 		// Encode the messages
 		System.arraycopy(groupId.getBytes(), 0, raw, 0, UniqueId.LENGTH);
 		ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH);
+
+		dependencies.add(messageId1);
+		states.put(messageId1, INVALID);
 	}
 
 	@Test
@@ -64,8 +87,10 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final Transaction txn = new Transaction(null, false);
 		final Transaction txn1 = new Transaction(null, false);
 		final Transaction txn2 = new Transaction(null, false);
+		final Transaction txn2b = new Transaction(null, false);
 		final Transaction txn3 = new Transaction(null, false);
 		final Transaction txn4 = new Transaction(null, false);
+		final Transaction txn5 = new Transaction(null, true);
 		context.checking(new Expectations() {{
 			// Get messages to validate
 			oneOf(db).startTransaction(true);
@@ -87,12 +112,22 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Store the validation result for the first message
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn2));
+			oneOf(db).getMessageDependencies(txn2, messageId);
 			oneOf(db).mergeMessageMetadata(txn2, messageId, metadata);
-			oneOf(db).setMessageValid(txn2, message, clientId, true);
-			oneOf(db).setMessageShared(txn2, message, true);
-			// Call the hook for the first message
-			oneOf(hook).incomingMessage(txn2, message, metadata);
+			oneOf(db).setMessageState(txn2, message, clientId, VALID);
 			oneOf(db).endTransaction(txn2);
+			// Async delivery
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn2b));
+			oneOf(db).setMessageShared(txn2b, message, true);
+			// Call the hook for the first message
+			oneOf(hook).incomingMessage(txn2b, message, metadata);
+			oneOf(db).getRawMessage(txn2b, messageId);
+			will(returnValue(raw));
+			oneOf(db).setMessageState(txn2b, message, clientId, DELIVERED);
+			oneOf(db).getMessageDependents(txn2b, messageId);
+			will(returnValue(Collections.emptyMap()));
+			oneOf(db).endTransaction(txn2b);
 			// Load the second raw message and group
 			oneOf(db).startTransaction(true);
 			will(returnValue(txn3));
@@ -107,8 +142,170 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Store the validation result for the second message
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn4));
-			oneOf(db).setMessageValid(txn4, message1, clientId, false);
+			oneOf(db).setMessageState(txn4, message1, clientId, INVALID);
+			// Recursively invalidate dependents
+			oneOf(db).getMessageDependents(txn4, messageId1);
+			oneOf(db).deleteMessage(txn4, messageId1);
+			oneOf(db).deleteMessageMetadata(txn4, messageId1);
+			oneOf(db).endTransaction(txn4);
+			// Get other messages to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn5));
+			oneOf(db).getMessagesToDeliver(txn5, clientId);
+			oneOf(db).getPendingMessages(txn5, clientId);
+			oneOf(db).endTransaction(txn5);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.startService();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testMessagesAreDeliveredAtStartup() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, true);
+		final Transaction txn2 = new Transaction(null, true);
+		final Transaction txn3 = new Transaction(null, false);
+		final Transaction txn4 = new Transaction(null, true);
+		final Transaction txn5 = new Transaction(null, false);
+
+		states.put(messageId1, PENDING);
+
+		context.checking(new Expectations() {{
+			// Get messages to validate
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getMessagesToValidate(txn, clientId);
+			oneOf(db).endTransaction(txn);
+			// Get IDs of messages to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn1));
+			oneOf(db).getMessagesToDeliver(txn1, clientId);
+			will(returnValue(Collections.singletonList(messageId)));
+			oneOf(db).getPendingMessages(txn1, clientId);
+			oneOf(db).endTransaction(txn1);
+			// Get message and its metadata to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn2));
+			oneOf(db).getRawMessage(txn2, messageId);
+			will(returnValue(message.getRaw()));
+			oneOf(db).getGroup(txn2, message.getGroupId());
+			will(returnValue(group));
+			oneOf(db).getMessageMetadata(txn2, messageId);
+			will(returnValue(metadata));
+			oneOf(db).endTransaction(txn2);
+			// Deliver message in a new transaction
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn3));
+			oneOf(db).setMessageShared(txn3, message, true);
+			oneOf(db).setMessageState(txn3, message, clientId, DELIVERED);
+			oneOf(hook).incomingMessage(txn3, message, metadata);
+			oneOf(db).getRawMessage(txn3, messageId);
+			will(returnValue(message.getRaw()));
+			// Try to also deliver pending dependents
+			oneOf(db).getMessageDependents(txn3, messageId);
+			will(returnValue(states));
+			oneOf(db).getMessageDependencies(txn3, messageId1);
+			will(returnValue(Collections.singletonMap(messageId2, DELIVERED)));
+			oneOf(db).endTransaction(txn3);
+			// Get the dependent to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn4));
+			oneOf(db).getRawMessage(txn4, messageId1);
+			will(returnValue(message1.getRaw()));
+			oneOf(db).getGroup(txn4, message.getGroupId());
+			will(returnValue(group));
+			oneOf(db).getMessageMetadata(txn4, messageId1);
+			will(returnValue(metadata));
 			oneOf(db).endTransaction(txn4);
+			// Deliver the dependent in a new transaction
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn5));
+			oneOf(db).setMessageShared(txn5, message1, true);
+			oneOf(db).setMessageState(txn5, message1, clientId, DELIVERED);
+			oneOf(hook).incomingMessage(txn5, message1, metadata);
+			oneOf(db).getRawMessage(txn5, messageId1);
+			will(returnValue(message1.getRaw()));
+			oneOf(db).getMessageDependents(txn5, messageId1);
+			oneOf(db).endTransaction(txn5);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.startService();
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+		assertTrue(txn3.isComplete());
+		assertTrue(txn4.isComplete());
+		assertTrue(txn5.isComplete());
+	}
+
+	@Test
+	public void testPendingMessagesAreDeliveredAtStartup() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, true);
+		final Transaction txn2 = new Transaction(null, true);
+		final Transaction txn3 = new Transaction(null, false);
+
+		context.checking(new Expectations() {{
+			// Get messages to validate
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getMessagesToValidate(txn, clientId);
+			oneOf(db).endTransaction(txn);
+			// Get IDs of messages to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn1));
+			oneOf(db).getMessagesToDeliver(txn1, clientId);
+			oneOf(db).getPendingMessages(txn1, clientId);
+			will(returnValue(Collections.singletonList(messageId)));
+			oneOf(db).endTransaction(txn1);
+			// Get message and its metadata to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn2));
+			oneOf(db).getRawMessage(txn2, messageId);
+			will(returnValue(message.getRaw()));
+			oneOf(db).getGroup(txn2, message.getGroupId());
+			will(returnValue(group));
+			oneOf(db).getMessageDependencies(txn2, messageId);
+			will(returnValue(Collections.singletonMap(messageId1, DELIVERED)));
+			oneOf(db).getMessageMetadata(txn2, messageId);
+			oneOf(db).endTransaction(txn2);
+			// Deliver the pending message
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn3));
+			oneOf(db).setMessageShared(txn3, message, true);
+			oneOf(db).setMessageState(txn3, message, clientId, DELIVERED);
+			oneOf(hook).incomingMessage(txn3, message, metadata);
+			oneOf(db).getRawMessage(txn3, messageId);
+			will(returnValue(message.getRaw()));
+			oneOf(db).getMessageDependents(txn3, messageId);
+			oneOf(db).endTransaction(txn3);
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
@@ -118,6 +315,11 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		vm.startService();
 
 		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+		assertTrue(txn3.isComplete());
 	}
 
 	@Test
@@ -134,6 +336,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final Transaction txn1 = new Transaction(null, true);
 		final Transaction txn2 = new Transaction(null, true);
 		final Transaction txn3 = new Transaction(null, false);
+		final Transaction txn4 = new Transaction(null, true);
 		context.checking(new Expectations() {{
 			// Get messages to validate
 			oneOf(db).startTransaction(true);
@@ -161,8 +364,18 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Store the validation result for the second message
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn3));
-			oneOf(db).setMessageValid(txn3, message1, clientId, false);
+			oneOf(db).setMessageState(txn3, message1, clientId, INVALID);
+			// recursively invalidate dependents
+			oneOf(db).getMessageDependents(txn3, messageId1);
+			oneOf(db).deleteMessage(txn3, messageId1);
+			oneOf(db).deleteMessageMetadata(txn3, messageId1);
 			oneOf(db).endTransaction(txn3);
+			// Get other messages to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn4));
+			oneOf(db).getMessagesToDeliver(txn4, clientId);
+			oneOf(db).getPendingMessages(txn4, clientId);
+			oneOf(db).endTransaction(txn4);
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
@@ -172,6 +385,12 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		vm.startService();
 
 		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+		assertTrue(txn3.isComplete());
+		assertTrue(txn4.isComplete());
 	}
 
 	@Test
@@ -188,6 +407,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final Transaction txn1 = new Transaction(null, true);
 		final Transaction txn2 = new Transaction(null, true);
 		final Transaction txn3 = new Transaction(null, false);
+		final Transaction txn4 = new Transaction(null, true);
 		context.checking(new Expectations() {{
 			// Get messages to validate
 			oneOf(db).startTransaction(true);
@@ -218,8 +438,18 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Store the validation result for the second message
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn3));
-			oneOf(db).setMessageValid(txn3, message1, clientId, false);
+			oneOf(db).setMessageState(txn3, message1, clientId, INVALID);
+			// recursively invalidate dependents
+			oneOf(db).getMessageDependents(txn3, messageId1);
+			oneOf(db).deleteMessage(txn3, messageId1);
+			oneOf(db).deleteMessageMetadata(txn3, messageId1);
 			oneOf(db).endTransaction(txn3);
+			// Get other messages to deliver
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn4));
+			oneOf(db).getMessagesToDeliver(txn4, clientId);
+			oneOf(db).getPendingMessages(txn4, clientId);
+			oneOf(db).endTransaction(txn4);
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
@@ -229,6 +459,12 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		vm.startService();
 
 		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+		assertTrue(txn3.isComplete());
+		assertTrue(txn4.isComplete());
 	}
 
 	@Test
@@ -242,6 +478,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 				context.mock(IncomingMessageHook.class);
 		final Transaction txn = new Transaction(null, true);
 		final Transaction txn1 = new Transaction(null, false);
+		final Transaction txn2 = new Transaction(null, false);
 		context.checking(new Expectations() {{
 			// Load the group
 			oneOf(db).startTransaction(true);
@@ -255,12 +492,21 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Store the validation result
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn1));
+			oneOf(db).getMessageDependencies(txn1, messageId);
 			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
-			oneOf(db).setMessageValid(txn1, message, clientId, true);
-			oneOf(db).setMessageShared(txn1, message, true);
-			// Call the hook
-			oneOf(hook).incomingMessage(txn1, message, metadata);
+			oneOf(db).setMessageState(txn1, message, clientId, VALID);
 			oneOf(db).endTransaction(txn1);
+			// async delivery
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn2));
+			oneOf(db).setMessageShared(txn2, message, true);
+			// Call the hook
+			oneOf(hook).incomingMessage(txn2, message, metadata);
+			oneOf(db).getRawMessage(txn2, messageId);
+			will(returnValue(raw));
+			oneOf(db).setMessageState(txn2, message, clientId, DELIVERED);
+			oneOf(db).getMessageDependents(txn2, messageId);
+			oneOf(db).endTransaction(txn2);
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
@@ -270,6 +516,10 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
 
 		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
 	}
 
 	@Test
@@ -290,4 +540,402 @@ public class ValidationManagerImplTest extends BriarTestCase {
 
 		context.assertIsSatisfied();
 	}
+
+	@Test
+	public void testMessagesWithNonDeliveredDependenciesArePending()
+			throws Exception {
+
+		states.put(messageId1, UNKNOWN);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, false);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(validResultWithDependencies));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn1));
+			oneOf(db).addMessageDependencies(txn1, message,
+					validResultWithDependencies.getDependencies());
+			oneOf(db).getMessageDependencies(txn1, messageId);
+			will(returnValue(states));
+			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
+			oneOf(db).setMessageState(txn1, message, clientId, PENDING);
+			oneOf(db).endTransaction(txn1);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+	}
+
+	@Test
+	public void testMessagesWithDeliveredDependenciesGetDelivered()
+			throws Exception {
+
+		states.put(messageId1, DELIVERED);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, false);
+		final Transaction txn2 = new Transaction(null, false);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(validResultWithDependencies));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn1));
+			oneOf(db).addMessageDependencies(txn1, message,
+					validResultWithDependencies.getDependencies());
+			oneOf(db).getMessageDependencies(txn1, messageId);
+			will(returnValue(states));
+			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
+			oneOf(db).setMessageState(txn1, message, clientId, VALID);
+			oneOf(db).endTransaction(txn1);
+			// async delivery
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn2));
+			oneOf(db).setMessageShared(txn2, message, true);
+			// Call the hook
+			oneOf(hook).incomingMessage(txn2, message, metadata);
+			oneOf(db).getRawMessage(txn2, messageId);
+			will(returnValue(raw));
+			oneOf(db).setMessageState(txn2, message, clientId, DELIVERED);
+			oneOf(db).getMessageDependents(txn2, messageId);
+			oneOf(db).endTransaction(txn2);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+	}
+
+	@Test
+	public void testMessagesWithInvalidDependenciesAreInvalid()
+			throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, false);
+		final Transaction txn2 = new Transaction(null, false);
+		final Transaction txn3 = new Transaction(null, true);
+		final Transaction txn4 = new Transaction(null, false);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(validResultWithDependencies));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn1));
+			oneOf(db).addMessageDependencies(txn1, message,
+					validResultWithDependencies.getDependencies());
+			oneOf(db).getMessageDependencies(txn1, messageId);
+			will(returnValue(states));
+			oneOf(db).endTransaction(txn1);
+			// Invalidate message in a new transaction
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn2));
+			oneOf(db).getMessageDependents(txn2, messageId);
+			will(returnValue(Collections.singletonMap(messageId2, UNKNOWN)));
+			oneOf(db).setMessageState(txn2, message, clientId, INVALID);
+			oneOf(db).deleteMessage(txn2, messageId);
+			oneOf(db).deleteMessageMetadata(txn2, messageId);
+			oneOf(db).endTransaction(txn2);
+			// Get message to invalidate in a new transaction
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn3));
+			oneOf(db).getRawMessage(txn3, messageId2);
+			will(returnValue(message2.getRaw()));
+			oneOf(db).getGroup(txn3, message2.getGroupId());
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn3);
+			// Invalidate dependent message in a new transaction
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn4));
+			oneOf(db).getMessageDependents(txn4, messageId2);
+			will(returnValue(Collections.emptyMap()));
+			oneOf(db).setMessageState(txn4, message2, clientId, INVALID);
+			oneOf(db).deleteMessage(txn4, messageId2);
+			oneOf(db).deleteMessageMetadata(txn4, messageId2);
+			oneOf(db).endTransaction(txn4);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+		assertTrue(txn3.isComplete());
+		assertTrue(txn4.isComplete());
+	}
+
+	@Test
+	public void testPendingDependentsGetDelivered() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, false);
+		final Transaction txn2 = new Transaction(null, false);
+		final Transaction txn3 = new Transaction(null, true);
+		final Transaction txn4 = new Transaction(null, false);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(validResult));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn1));
+			oneOf(db).getMessageDependencies(txn1, messageId);
+			will(returnValue(Collections.emptyMap()));
+			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
+			oneOf(db).setMessageState(txn1, message, clientId, VALID);
+			oneOf(db).endTransaction(txn1);
+			// Deliver first message
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn2));
+			oneOf(db).setMessageShared(txn2, message, true);
+			oneOf(hook).incomingMessage(txn2, message, metadata);
+			oneOf(db).getRawMessage(txn2, messageId);
+			will(returnValue(raw));
+			oneOf(db).setMessageState(txn2, message, clientId, DELIVERED);
+			oneOf(db).getMessageDependents(txn2, messageId);
+			will(returnValue(Collections.singletonMap(messageId1, PENDING)));
+			oneOf(db).getMessageDependencies(txn2, messageId1);
+			will(returnValue(Collections.singletonMap(messageId2, DELIVERED)));
+			oneOf(db).endTransaction(txn2);
+			// Also get the pending message for delivery
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn3));
+			oneOf(db).getRawMessage(txn3, messageId1);
+			will(returnValue(message1.getRaw()));
+			oneOf(db).getGroup(txn3, message1.getGroupId());
+			will(returnValue(group));
+			oneOf(db).getMessageMetadata(txn3, messageId1);
+			will(returnValue(metadata));
+			oneOf(db).endTransaction(txn3);
+			// Deliver the pending message
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn4));
+			oneOf(db).setMessageShared(txn4, message1, true);
+			oneOf(hook).incomingMessage(txn4, message1, metadata);
+			oneOf(db).getRawMessage(txn4, messageId1);
+			will(returnValue(raw));
+			oneOf(db).setMessageState(txn4, message1, clientId, DELIVERED);
+			oneOf(db).getMessageDependents(txn4, messageId1);
+			will(returnValue(Collections.emptyMap()));
+			oneOf(db).endTransaction(txn4);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+		assertTrue(txn3.isComplete());
+		assertTrue(txn4.isComplete());
+	}
+
+	@Test
+	public void testOnlyReadyPendingDependentsGetDelivered() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, false);
+		final Transaction txn2 = new Transaction(null, false);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(validResult));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn1));
+			oneOf(db).getMessageDependencies(txn1, messageId);
+			will(returnValue(Collections.emptyMap()));
+			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
+			oneOf(db).setMessageState(txn1, message, clientId, VALID);
+			oneOf(db).endTransaction(txn1);
+			// Deliver first message
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn2));
+			oneOf(db).setMessageShared(txn2, message, true);
+			oneOf(hook).incomingMessage(txn2, message, metadata);
+			oneOf(db).getRawMessage(txn2, messageId);
+			will(returnValue(raw));
+			oneOf(db).setMessageState(txn2, message, clientId, DELIVERED);
+			oneOf(db).getMessageDependents(txn2, messageId);
+			will(returnValue(Collections.singletonMap(messageId1, PENDING)));
+			oneOf(db).getMessageDependencies(txn2, messageId1);
+			will(returnValue(Collections.singletonMap(messageId2, VALID)));
+			oneOf(db).endTransaction(txn2);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+		assertTrue(txn2.isComplete());
+	}
+
+	@Test
+	public void testMessageDependencyCycle() throws Exception {
+		states.put(messageId1, UNKNOWN);
+		final MessageContext cycleContext = new MessageContext(metadata,
+				Collections.singletonList(messageId));
+
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final IncomingMessageHook hook =
+				context.mock(IncomingMessageHook.class);
+		final Transaction txn = new Transaction(null, true);
+		final Transaction txn1 = new Transaction(null, false);
+		final Transaction txn2 = new Transaction(null, true);
+		final Transaction txn3 = new Transaction(null, false);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(validResultWithDependencies));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn1));
+			oneOf(db).addMessageDependencies(txn1, message,
+					validResultWithDependencies.getDependencies());
+			oneOf(db).getMessageDependencies(txn1, messageId);
+			will(returnValue(states));
+			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
+			oneOf(db).setMessageState(txn1, message, clientId, PENDING);
+			oneOf(db).endTransaction(txn1);
+			// Second message is coming in
+			oneOf(db).startTransaction(true);
+			will(returnValue(txn2));
+			oneOf(db).getGroup(txn2, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn2);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message1, group);
+			will(returnValue(cycleContext));
+			// Store the validation result
+			oneOf(db).startTransaction(false);
+			will(returnValue(txn3));
+			oneOf(db).addMessageDependencies(txn3, message1,
+					cycleContext.getDependencies());
+			oneOf(db).getMessageDependencies(txn3, messageId1);
+			will(returnValue(Collections.singletonMap(messageId, PENDING)));
+			oneOf(db).mergeMessageMetadata(txn3, messageId1, metadata);
+			oneOf(db).setMessageState(txn3, message1, clientId, PENDING);
+			oneOf(db).endTransaction(txn3);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerIncomingMessageHook(clientId, hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+		vm.eventOccurred(new MessageAddedEvent(message1, contactId));
+
+		context.assertIsSatisfied();
+
+		assertTrue(txn.isComplete());
+		assertTrue(txn1.isComplete());
+	}
+
 }