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..b4a9c78db6b7cea9caee363f37ef01a63fd5d151 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;
@@ -60,6 +60,7 @@ 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.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -669,14 +670,13 @@ 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()
+			if (e instanceof MessageStateChangedEvent) {
+				MessageStateChangedEvent event = (MessageStateChangedEvent) e;
+				if (event.getState() == DELIVERED && event.getClientId()
 						.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 +722,13 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
 		}
 
 		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(forumSharingManager1.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..043fcae7d5774ddc218b3fef7ea772c207938236 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;
@@ -70,6 +70,7 @@ 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.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -1047,15 +1048,14 @@ 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: Introducee" + introducee +
 							" received message in group " +
-							((MessageValidatedEvent) e).getMessage()
-									.getGroupId().hashCode());
+							event.getMessage().getGroupId().hashCode());
 					msgWaiter.resume();
 				}
 			} else if (e instanceof IntroductionRequestReceivedEvent) {
@@ -1114,14 +1114,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..2fbd55f4ac8523571f361b3e8df105691a90fe56 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,7 +79,7 @@ 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;
 
 	/**
@@ -326,6 +326,24 @@ interface Database<T> {
 	MessageStatus getMessageStatus(T txn, ContactId c, MessageId m)
 			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}.
+	 * <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 some messages received from the given contact that
 	 * need to be acknowledged, up to the given number of messages.
@@ -371,6 +389,24 @@ interface Database<T> {
 	Collection<MessageId> getMessagesToValidate(T 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(T 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(T txn, ClientId c)
+			throws DbException;
+
 	/**
 	 * Returns the message with the given ID, in serialised form, or null if
 	 * the message has been deleted.
@@ -538,7 +574,14 @@ 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;
+
+	/*
+	 * Adds a dependency between two MessageIds
+	 */
+	void addMessageDependency(T txn, MessageId dependentId,
+			MessageId dependencyId) 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/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 1e85803a22aba9d836f5564b23b3fb8854f10917..487c31d52c003ec854d808f32719648e4e52f3f2 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -36,6 +36,8 @@ 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.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
 
 class ValidationManagerImpl implements ValidationManager, Service,
 		EventListener {
@@ -178,7 +180,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 					try {
 						Metadata meta = result.getMetadata();
 						db.mergeMessageMetadata(txn, m.getId(), meta);
-						db.setMessageValid(txn, m, c, true);
+						db.setMessageState(txn, m.getId(), c, VALID);
 						db.setMessageShared(txn, m, true);
 						IncomingMessageHook hook = hooks.get(c);
 						if (hook != null)
@@ -202,7 +204,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 				try {
 					Transaction txn = db.startTransaction(false);
 					try {
-						db.setMessageValid(txn, m, c, false);
+						db.setMessageState(txn, m.getId(), c, INVALID);
 						txn.setComplete();
 					} finally {
 						db.endTransaction(txn);
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..d55b575c2d9c2d1db66aadbb6ad1326631fb2a83 100644
--- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
@@ -28,6 +28,9 @@ import org.junit.Test;
 import java.util.Arrays;
 import java.util.concurrent.Executor;
 
+import static org.briarproject.api.sync.ValidationManager.State.INVALID;
+import static org.briarproject.api.sync.ValidationManager.State.VALID;
+
 public class ValidationManagerImplTest extends BriarTestCase {
 
 	private final ClientId clientId = new ClientId(TestUtils.getRandomId());
@@ -88,7 +91,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn2));
 			oneOf(db).mergeMessageMetadata(txn2, messageId, metadata);
-			oneOf(db).setMessageValid(txn2, message, clientId, true);
+			oneOf(db).setMessageState(txn2, messageId, clientId, VALID);
 			oneOf(db).setMessageShared(txn2, message, true);
 			// Call the hook for the first message
 			oneOf(hook).incomingMessage(txn2, message, metadata);
@@ -107,7 +110,7 @@ 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, messageId1, clientId, INVALID);
 			oneOf(db).endTransaction(txn4);
 		}});
 
@@ -161,7 +164,7 @@ 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, messageId1, clientId, INVALID);
 			oneOf(db).endTransaction(txn3);
 		}});
 
@@ -218,7 +221,7 @@ 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, messageId1, clientId, INVALID);
 			oneOf(db).endTransaction(txn3);
 		}});
 
@@ -256,7 +259,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			oneOf(db).startTransaction(false);
 			will(returnValue(txn1));
 			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
-			oneOf(db).setMessageValid(txn1, message, clientId, true);
+			oneOf(db).setMessageState(txn1, messageId, clientId, VALID);
 			oneOf(db).setMessageShared(txn1, message, true);
 			// Call the hook
 			oneOf(hook).incomingMessage(txn1, message, metadata);