diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index 852affa2db17b4edef84ff581f5bd8fdedefd0a7..0e51240b20753856455beee07dd86036c881736a 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -56,7 +56,7 @@ public interface DatabaseComponent {
 	void addLocalAuthor(LocalAuthor a) throws DbException;
 
 	/** Stores a local message. */
-	void addLocalMessage(Message m, ClientId c, Metadata meta)
+	void addLocalMessage(Message m, ClientId c, Metadata meta, boolean shared)
 			throws DbException;
 
 	/**
@@ -277,8 +277,11 @@ public interface DatabaseComponent {
 	void setLocalAuthorStatus(AuthorId a, StorageStatus s)
 		throws DbException;
 
+	/** Marks the given message as shared or unshared. */
+	void setMessageShared(Message m, boolean shared) throws DbException;
+
 	/** Marks the given message as valid or invalid. */
-	void setMessageValidity(Message m, ClientId c, boolean valid)
+	void setMessageValid(Message m, ClientId c, boolean valid)
 			throws DbException;
 
 	/**
diff --git a/briar-api/src/org/briarproject/api/event/MessageSharedEvent.java b/briar-api/src/org/briarproject/api/event/MessageSharedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..d011e8c3ebe5394e2dccb13c22f423c827db725e
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/MessageSharedEvent.java
@@ -0,0 +1,17 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.sync.Message;
+
+/** An event that is broadcast when a message is shared. */
+public class MessageSharedEvent extends Event {
+
+	private final Message message;
+
+	public MessageSharedEvent(Message message) {
+		this.message = message;
+	}
+
+	public Message getMessage() {
+		return message;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
index 4de322bc0c793d13f4cfd80220a81f47448a5302..88be9d4c82de2b427fce92f1f55264785f5c0841 100644
--- a/briar-api/src/org/briarproject/api/sync/ValidationManager.java
+++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
@@ -6,13 +6,13 @@ package org.briarproject.api.sync;
  */
 public interface ValidationManager {
 
-	enum Status {
+	enum Validity {
 
 		UNKNOWN(0), INVALID(1), VALID(2);
 
 		private final int value;
 
-		Status(int value) {
+		Validity(int value) {
 			this.value = value;
 		}
 
@@ -20,8 +20,8 @@ public interface ValidationManager {
 			return value;
 		}
 
-		public static Status fromValue(int value) {
-			for (Status s : values()) if (s.value == value) return s;
+		public static Validity fromValue(int value) {
+			for (Validity 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 f8901b01abb3f22a7247b41ef1451705a5ddef23..22cc9357f61a4065de80172b6cfad88449d67c8f 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -19,6 +19,7 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.sync.SubscriptionAck;
 import org.briarproject.api.sync.SubscriptionUpdate;
+import org.briarproject.api.sync.ValidationManager.Validity;
 import org.briarproject.api.transport.TransportKeys;
 
 import java.io.IOException;
@@ -111,7 +112,8 @@ interface Database<T> {
 	 * <p>
 	 * Locking: write.
 	 */
-	void addMessage(T txn, Message m, boolean local) throws DbException;
+	void addMessage(T txn, Message m, Validity validity, boolean shared)
+			throws DbException;
 
 	/**
 	 * Records that a message has been offered by the given contact.
@@ -618,13 +620,20 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Marks the given message as valid or invalid.
+	 * Marks the given message as shared or unshared.
 	 * <p>
 	 * Locking: write.
 	 */
-	void setMessageValidity(T txn, MessageId m, boolean valid)
+	void setMessageShared(T txn, MessageId m, boolean shared)
 			throws DbException;
 
+	/**
+	 * Marks the given message as valid or invalid.
+	 * <p>
+	 * Locking: write.
+	 */
+	void setMessageValid(T txn, MessageId m, boolean valid) throws DbException;
+
 	/**
 	 * Sets the reordering window for the given contact and transport in the
 	 * given rotation period.
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 9cce3d314766f18b3db3371d000e2e11bec5befa..b303939cd3e813826af029b06966332b1ec1efd7 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -20,6 +20,7 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
 import org.briarproject.api.event.MessageAddedEvent;
 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;
@@ -47,6 +48,7 @@ import org.briarproject.api.sync.Offer;
 import org.briarproject.api.sync.Request;
 import org.briarproject.api.sync.SubscriptionAck;
 import org.briarproject.api.sync.SubscriptionUpdate;
+import org.briarproject.api.sync.ValidationManager.Validity;
 import org.briarproject.api.transport.TransportKeys;
 
 import java.io.IOException;
@@ -64,6 +66,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.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 
 /**
@@ -214,8 +218,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public void addLocalMessage(Message m, ClientId c, Metadata meta)
-			throws DbException {
+	public void addLocalMessage(Message m, ClientId c, Metadata meta,
+			boolean shared) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
@@ -224,7 +228,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 					throw new MessageExistsException();
 				if (!db.containsGroup(txn, m.getGroupId()))
 					throw new NoSuchSubscriptionException();
-				addMessage(txn, m, null);
+				addMessage(txn, m, VALID, shared, null);
 				db.mergeMessageMetadata(txn, m.getId(), meta);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
@@ -244,9 +248,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	 * Locking: write.
 	 * @param sender null for a locally generated message.
 	 */
-	private void addMessage(T txn, Message m, ContactId sender)
-			throws DbException {
-		db.addMessage(txn, m, sender == null);
+	private void addMessage(T txn, Message m, Validity validity, boolean shared,
+			ContactId sender) throws DbException {
+		db.addMessage(txn, m, validity, shared);
 		GroupId g = m.getGroupId();
 		Collection<ContactId> visibility = db.getVisibility(txn, g);
 		visibility = new HashSet<ContactId>(visibility);
@@ -982,7 +986,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				duplicate = db.containsMessage(txn, m.getId());
 				visible = db.containsVisibleGroup(txn, c, m.getGroupId());
 				if (visible) {
-					if (!duplicate) addMessage(txn, m, c);
+					if (!duplicate) addMessage(txn, m, UNKNOWN, true, c);
 					db.raiseAckFlag(txn, c, m.getId());
 				}
 				db.commitTransaction(txn);
@@ -1214,7 +1218,27 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
-	public void setMessageValidity(Message m, ClientId c, boolean valid)
+	public void setMessageShared(Message m, boolean shared)
+			throws DbException {
+		lock.writeLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				if (!db.containsMessage(txn, m.getId()))
+					throw new NoSuchMessageException();
+				db.setMessageShared(txn, m.getId(), shared);
+				db.commitTransaction(txn);
+			} catch (DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			lock.writeLock().unlock();
+		}
+		if (shared) eventBus.broadcast(new MessageSharedEvent(m));
+	}
+
+	public void setMessageValid(Message m, ClientId c, boolean valid)
 			throws DbException {
 		lock.writeLock().lock();
 		try {
@@ -1222,7 +1246,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			try {
 				if (!db.containsMessage(txn, m.getId()))
 					throw new NoSuchMessageException();
-				db.setMessageValidity(txn, m.getId(), valid);
+				db.setMessageValid(txn, m.getId(), valid);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index e79e51932a4902b7014a315d85b47a6f4fbd55c2..045cb198f8a56bc972d2bb14ccce354e353f62ed 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -22,6 +22,7 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.sync.SubscriptionAck;
 import org.briarproject.api.sync.SubscriptionUpdate;
+import org.briarproject.api.sync.ValidationManager.Validity;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.transport.IncomingKeys;
 import org.briarproject.api.transport.OutgoingKeys;
@@ -54,9 +55,9 @@ import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.db.Metadata.REMOVE;
 import static org.briarproject.api.db.StorageStatus.ADDING;
 import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS;
-import static org.briarproject.api.sync.ValidationManager.Status.INVALID;
-import static org.briarproject.api.sync.ValidationManager.Status.UNKNOWN;
-import static org.briarproject.api.sync.ValidationManager.Status.VALID;
+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.db.DatabaseConstants.DB_SETTINGS_NAMESPACE;
 import static org.briarproject.db.DatabaseConstants.DEVICE_ID_KEY;
 import static org.briarproject.db.DatabaseConstants.DEVICE_SETTINGS_NAMESPACE;
@@ -70,8 +71,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 18;
-	private static final int MIN_SCHEMA_VERSION = 18;
+	private static final int SCHEMA_VERSION = 19;
+	private static final int MIN_SCHEMA_VERSION = 19;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -164,8 +165,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " (messageId HASH NOT NULL,"
 					+ " groupId HASH NOT NULL,"
 					+ " timestamp BIGINT NOT NULL,"
-					+ " local BOOLEAN NOT NULL,"
 					+ " valid INT NOT NULL,"
+					+ " shared BOOLEAN NOT NULL,"
 					+ " length INT NOT NULL,"
 					+ " raw BLOB NOT NULL,"
 					+ " PRIMARY KEY (messageId),"
@@ -674,19 +675,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addMessage(Connection txn, Message m, boolean local)
-			throws DbException {
+	public void addMessage(Connection txn, Message m, Validity validity,
+			boolean shared) throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
-					+ " local, valid, length, raw)"
+					+ " valid, 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.setBoolean(4, local);
-			ps.setInt(5, local ? VALID.getValue() : UNKNOWN.getValue());
+			ps.setInt(4, validity.getValue());
+			ps.setBoolean(5, shared);
 			byte[] raw = m.getRaw();
 			ps.setInt(6, raw.length);
 			ps.setBytes(7, raw);
@@ -1031,7 +1032,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " JOIN groupVisibilities AS gv"
 					+ " ON m.groupId = gv.groupId"
 					+ " WHERE messageId = ?"
-					+ " AND contactId = ?";
+					+ " AND contactId = ?"
+					+ " AND shared = TRUE";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1482,7 +1484,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " ON m.messageId = s.messageId"
 					+ " AND cg.contactId = s.contactId"
 					+ " WHERE cg.contactId = ?"
-					+ " AND valid = ?"
+					+ " AND valid = ? AND shared = TRUE"
 					+ " AND seen = FALSE AND requested = FALSE"
 					+ " AND s.expiry < ?"
 					+ " ORDER BY timestamp DESC LIMIT ?";
@@ -1544,7 +1546,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " ON m.messageId = s.messageId"
 					+ " AND cg.contactId = s.contactId"
 					+ " WHERE cg.contactId = ?"
-					+ " AND valid = ?"
+					+ " AND valid = ? AND shared = TRUE"
 					+ " AND seen = FALSE"
 					+ " AND s.expiry < ?"
 					+ " ORDER BY timestamp DESC";
@@ -1633,7 +1635,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " ON m.messageId = s.messageId"
 					+ " AND cg.contactId = s.contactId"
 					+ " WHERE cg.contactId = ?"
-					+ " AND valid = ?"
+					+ " AND valid = ? AND shared = TRUE"
 					+ " AND seen = FALSE AND requested = TRUE"
 					+ " AND s.expiry < ?"
 					+ " ORDER BY timestamp DESC";
@@ -1660,7 +1662,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Settings getSettings(Connection txn, String namespace) throws DbException {
+	public Settings getSettings(Connection txn, String namespace)
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -2397,7 +2400,24 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setMessageValidity(Connection txn, MessageId m, boolean valid)
+	public void setMessageShared(Connection txn, MessageId m, boolean shared)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "UPDATE messages SET shared = ? WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBoolean(1, shared);
+			ps.setBytes(2, m.getBytes());
+			int affected = ps.executeUpdate();
+			if (affected < 0) throw new DbStateException();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public void setMessageValid(Connection txn, MessageId m, boolean valid)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index ba604e93809ff82de3d26a2503268bcbe3834c62..6b846d6714153d21a1b8ed36c01ab4ace2dfc633 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -135,7 +135,7 @@ class ForumManagerImpl implements ForumManager {
 		d.put("read", true);
 		try {
 			Metadata meta = metadataEncoder.encode(d);
-			db.addLocalMessage(p.getMessage(), CLIENT_ID, meta);
+			db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true);
 		} catch (FormatException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 90c8f426e2f6e7b680991e07f659e8fde2d8f98d..ea1a852a90303d5f8072982f81e3fb9450fb8825 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -116,7 +116,7 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 		d.put("read", true);
 		try {
 			Metadata meta = metadataEncoder.encode(d);
-			db.addLocalMessage(m.getMessage(), CLIENT_ID, meta);
+			db.addLocalMessage(m.getMessage(), CLIENT_ID, meta, true);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index 10328e3c26c3bc23b39131a88f368fbe79c9da89..9d42269ada92b2e8c8f643e261d617fd5570300b 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -130,7 +130,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		d.put("transportId", t.getString());
 		d.put("version", version);
 		d.put("local", true);
-		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d));
+		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), true);
 	}
 
 	private byte[] encodeProperties(DeviceId dev, TransportId t,
diff --git a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
index 52528de6012720b86f5f6ee2d50190d4859569d8..29725fe81741c293093578e302692231cee403e3 100644
--- a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
@@ -10,6 +10,7 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
 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;
@@ -154,6 +155,8 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 		if (e instanceof ContactRemovedEvent) {
 			ContactRemovedEvent c = (ContactRemovedEvent) e;
 			if (c.getContactId().equals(contactId)) interrupt();
+		} else if (e instanceof MessageSharedEvent) {
+			dbExecutor.execute(new GenerateOffer());
 		} else if (e instanceof MessageValidatedEvent) {
 			if (((MessageValidatedEvent) e).isValid())
 				dbExecutor.execute(new GenerateOffer());
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 28e248ae42fbd79b59f777978db4e8fa6ebf9109..3dcd616e047e03324f8be610a36e411330c3f33b 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -117,10 +117,10 @@ class ValidationManagerImpl implements ValidationManager, Service,
 			public void run() {
 				try {
 					if (meta == null) {
-						db.setMessageValidity(m, c, false);
+						db.setMessageValid(m, c, false);
 					} else {
 						db.mergeMessageMetadata(m.getId(), meta);
-						db.setMessageValidity(m, c, true);
+						db.setMessageValid(m, c, true);
 					}
 				} catch (NoSuchMessageException e) {
 					LOG.info("Message removed during validation");
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index 37998807a7bf4e3117215c0076e1eddc6fa1e834..eb0d8315c05224507ae5828488c055ad2040b25c 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -53,6 +53,8 @@ import java.util.Collections;
 
 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.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -211,7 +213,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 				shutdown);
 
 		try {
-			db.addLocalMessage(message, clientId, metadata);
+			db.addLocalMessage(message, clientId, metadata, true);
 			fail();
 		} catch (MessageExistsException expected) {
 			// Expected
@@ -241,7 +243,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 				shutdown);
 
 		try {
-			db.addLocalMessage(message, clientId, metadata);
+			db.addLocalMessage(message, clientId, metadata, true);
 			fail();
 		} catch (NoSuchSubscriptionException expected) {
 			// Expected
@@ -264,7 +266,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(false));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).addMessage(txn, message, true);
+			oneOf(database).addMessage(txn, message, VALID, true);
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
@@ -281,7 +283,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
 
-		db.addLocalMessage(message, clientId, metadata);
+		db.addLocalMessage(message, clientId, metadata, true);
 
 		context.assertIsSatisfied();
 	}
@@ -559,11 +561,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(4).of(database).startTransaction();
+			exactly(6).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(4).of(database).containsMessage(txn, messageId);
+			exactly(6).of(database).containsMessage(txn, messageId);
 			will(returnValue(false));
-			exactly(4).of(database).abortTransaction(txn);
+			exactly(6).of(database).abortTransaction(txn);
 			// This is needed for getMessageStatus() to proceed
 			exactly(1).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
@@ -599,6 +601,20 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.setMessageShared(message, true);
+			fail();
+		} catch (NoSuchMessageException expected) {
+			// Expected
+		}
+
+		try {
+			db.setMessageValid(message, clientId, true);
+			fail();
+		} catch (NoSuchMessageException expected) {
+			// Expected
+		}
+
 		context.assertIsSatisfied();
 	}
 
@@ -932,7 +948,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(false));
 			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
 			will(returnValue(true));
-			oneOf(database).addMessage(txn, message, false);
+			oneOf(database).addMessage(txn, message, UNKNOWN, true);
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).getContactIds(txn);
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index 5dd1208986b9edbfecd0f191385263bbf5d7336a..eb1bbb336c2ac938128919e927655c3e18c8e302 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -34,7 +34,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -47,6 +46,8 @@ 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.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -113,7 +114,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroup(txn, group);
 		assertTrue(db.containsGroup(txn, groupId));
 		assertFalse(db.containsMessage(txn, messageId));
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		assertTrue(db.containsMessage(txn, messageId));
 		db.commitTransaction(txn);
 		db.close();
@@ -126,6 +127,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertTrue(db.containsMessage(txn, messageId));
 		byte[] raw1 = db.getRawMessage(txn, messageId);
 		assertArrayEquals(raw, raw1);
+
 		// Delete the records
 		db.removeMessage(txn, messageId);
 		db.removeContact(txn, contactId);
@@ -150,7 +152,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to a group and store a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 
 		// Unsubscribing from the group should remove the message
 		assertTrue(db.containsMessage(txn, messageId));
@@ -172,26 +174,106 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 
 		// The message has no status yet, so it should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
 				ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
 
 		// Adding a status with seen = false should make the message sendable
 		db.addStatus(txn, contactId, messageId, false, false);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertFalse(ids.isEmpty());
-		Iterator<MessageId> it = ids.iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
 
 		// Changing the status to seen = true should make the message unsendable
 		db.raiseSeenFlag(txn, contactId, messageId);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeValid() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, subscribe to a group and store an unvalidated message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		db.addGroup(txn, group);
+		db.addVisibility(txn, contactId, groupId);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
+		db.addMessage(txn, message, UNKNOWN, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message has not been validated, so it should not be sendable
+		Collection<MessageId> 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 sendable
+		db.setMessageValid(txn, messageId, true);
+		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);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testSendableMessagesMustBeShared() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, subscribe to a group and store an unshared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		db.addGroup(txn, group);
+		db.addVisibility(txn, contactId, groupId);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
+		db.addMessage(txn, message, VALID, false);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The message is not shared, so it should not be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Sharing the message should make it sendable
+		db.setMessageShared(txn, messageId, true);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// Unsharing the message should make it unsendable
+		db.setMessageShared(txn, messageId, false);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
 		db.close();
@@ -207,27 +289,29 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The contact is not subscribed, so the message should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
 				ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
 
 		// The contact subscribing should make the message sendable
 		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertFalse(ids.isEmpty());
-		Iterator<MessageId> it = ids.iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
 
 		// The contact unsubscribing should make the message unsendable
 		db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
 		db.close();
@@ -244,7 +328,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The message is sendable, but too large to send
@@ -254,11 +338,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// The message is just the right size to send
 		ids = db.getMessagesToSend(txn, contactId, size);
-		assertFalse(ids.isEmpty());
-		Iterator<MessageId> it = ids.iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
+		assertEquals(Collections.singletonList(messageId), ids);
 
 		db.commitTransaction(txn);
 		db.close();
@@ -274,7 +354,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The subscription is not visible to the contact, so the message
@@ -282,15 +362,22 @@ public class H2DatabaseTest extends BriarTestCase {
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
 				ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
 
 		// Making the subscription visible should make the message sendable
 		db.addVisibility(txn, contactId, groupId);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertFalse(ids.isEmpty());
-		Iterator<MessageId> it = ids.iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// Making the subscription invisible should make the message unsendable
+		db.removeVisibility(txn, contactId, groupId);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
 		db.close();
@@ -310,16 +397,16 @@ 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, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, true);
 		db.raiseAckFlag(txn, contactId, messageId);
-		db.addMessage(txn, message1, true);
+		db.addMessage(txn, message1, VALID, true);
 		db.addStatus(txn, contactId, messageId1, false, true);
 		db.raiseAckFlag(txn, contactId, messageId1);
 
 		// Both message IDs should be returned
-		Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
-		assertEquals(ids, db.getMessagesToAck(txn, contactId, 1234));
+		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
+		assertEquals(Arrays.asList(messageId, messageId1), ids);
 
 		// Remove both message IDs
 		db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1));
@@ -332,38 +419,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testDuplicateMessageReceived() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact and subscribe to a group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-
-		// Receive the same message twice
-		db.addMessage(txn, message, true);
-		db.addStatus(txn, contactId, messageId, false, true);
-		db.raiseAckFlag(txn, contactId, messageId);
-		db.raiseAckFlag(txn, contactId, messageId);
-
-		// The message ID should only be returned once
-		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
-		assertEquals(Collections.singletonList(messageId), ids);
-
-		// Remove the message ID
-		db.lowerAckFlag(txn, contactId, Collections.singletonList(messageId));
-
-		// The message ID should have been removed
-		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
-				contactId, 1234));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testOutstandingMessageAcked() throws Exception {
 		Database<Connection> db = open(false);
@@ -375,27 +430,25 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// Retrieve the message from the database and mark it as sent
-		Iterator<MessageId> it =
-				db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
-		assertTrue(it.hasNext());
-		assertEquals(messageId, it.next());
-		assertFalse(it.hasNext());
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
 		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
 
 		// The message should no longer be sendable
-		it = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
-		assertFalse(it.hasNext());
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
 
 		// Pretend that the message was acked
 		db.raiseSeenFlag(txn, contactId, messageId);
 
 		// The message still should not be sendable
-		it = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
-		assertFalse(it.hasNext());
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
 
 		db.commitTransaction(txn);
 		db.close();
@@ -419,7 +472,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, true);
+		db.addMessage(txn, message, VALID, true);
 		db.commitTransaction(txn);
 		assertTrue(db.getFreeSpace() < free);
 
@@ -579,7 +632,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The subscription is not visible
@@ -904,7 +957,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addVisibility(txn, contactId, groupId);
 
 		// Add a message - it should be sendable to the contact
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 		Collection<MessageId> sendable = db.getMessagesToSend(txn, contactId,
 				ONE_MEGABYTE);
@@ -974,7 +1027,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 
 		// Attach some metadata to the message
 		Metadata metadata = new Metadata();
@@ -1042,7 +1095,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addVisibility(txn, contactId, groupId);
 
 		// Add a message to the group
-		db.addMessage(txn, message, true);
+		db.addMessage(txn, message, VALID, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
 		// The message should not be sent or seen