diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
index 04c3404ded64bc0cbbae35918a30ba7f09ed1506..79076e317469c5396f648f46dc67ad1f87fe51d6 100644
--- a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
@@ -118,7 +118,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
 					Collection<GroupMessageHeader> headers =
-							db.getMessageHeaders(groupId);
+							db.getGroupMessageHeaders(groupId);
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
index 5f35cc3e9157f5638b5858104c521e4806c1d475..2beedf041873bbaa322e65980ed95b7cb170bfc3 100644
--- a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
@@ -129,7 +129,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 						boolean postable = local.contains(g.getId());
 						try {
 							Collection<GroupMessageHeader> headers =
-									db.getMessageHeaders(g.getId());
+									db.getGroupMessageHeaders(g.getId());
 							displayHeaders(g, postable, headers);
 						} catch(NoSuchSubscriptionException e) {
 							if(LOG.isLoggable(INFO))
@@ -256,7 +256,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
 					Collection<GroupMessageHeader> headers =
-							db.getMessageHeaders(g.getId());
+							db.getGroupMessageHeaders(g.getId());
 					boolean postable = db.getLocalGroups().contains(g);
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
index 0cdd423055dccde1550d623c11fe7be6d7770ece..ee389d2feba68f65ecf43a16a9b76c5e7b0fef6b 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -116,7 +116,7 @@ OnClickListener, OnItemClickListener {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
 					Collection<GroupMessageHeader> headers =
-							db.getMessageHeaders(groupId);
+							db.getGroupMessageHeaders(groupId);
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
index 83f3c3edd56c81e9203a18a922fea25daaaa77ec..3deda412db42abff09c6296f61764dccc788d363 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -124,7 +124,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 						if(g.isRestricted()) continue;
 						try {
 							Collection<GroupMessageHeader> headers =
-									db.getMessageHeaders(g.getId());
+									db.getGroupMessageHeaders(g.getId());
 							displayHeaders(g, headers);
 						} catch(NoSuchSubscriptionException e) {
 							if(LOG.isLoggable(INFO))
@@ -244,7 +244,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
 					Collection<GroupMessageHeader> headers =
-							db.getMessageHeaders(g.getId());
+							db.getGroupMessageHeaders(g.getId());
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Partial load took " + duration + " ms");
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
index fca12f6e4288b4c6a06a4dbdccb6f59831419d98..e93e9f19fe04bf79f9a2d93b94bd50117a2e2bde 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
@@ -19,7 +19,6 @@ import net.sf.briar.android.BriarService.BriarServiceConnection;
 import net.sf.briar.android.widgets.HorizontalBorder;
 import net.sf.briar.api.AuthorId;
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.LocalAuthor;
 import net.sf.briar.api.android.DatabaseUiExecutor;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
@@ -60,7 +59,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
 	private volatile ContactId contactId = null;
 	private volatile AuthorId localAuthorId = null;
-	private volatile String localAuthorName = null;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -82,7 +80,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		layout.setOrientation(VERTICAL);
 		layout.setGravity(CENTER_HORIZONTAL);
 
-		adapter = new ConversationAdapter(this, contactName);
+		adapter = new ConversationAdapter(this);
 		list = new ListView(this);
 		// Give me all the width and all the unused height
 		list.setLayoutParams(MATCH_WRAP_1);
@@ -118,14 +116,12 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 				try {
 					serviceConnection.waitForStartup();
 					long now = System.currentTimeMillis();
-					LocalAuthor localAuthor = db.getLocalAuthor(localAuthorId);
-					localAuthorName = localAuthor.getName();
 					Collection<PrivateMessageHeader> headers =
 							db.getPrivateMessageHeaders(contactId);
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
-					displayHeaders(localAuthor, headers);
+					displayHeaders(headers);
 				} catch(NoSuchContactException e) {
 					if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
 					finishOnUiThread();
@@ -141,11 +137,10 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		});
 	}
 
-	private void displayHeaders(final LocalAuthor localAuthor,
+	private void displayHeaders(
 			final Collection<PrivateMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				adapter.setLocalAuthorName(localAuthor.getName());
 				adapter.clear();
 				for(PrivateMessageHeader h : headers) adapter.add(h);
 				adapter.sort(AscendingHeaderComparator.INSTANCE);
@@ -228,7 +223,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		Intent i = new Intent(this, ReadPrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 		i.putExtra("net.sf.briar.CONTACT_NAME", contactName);
-		i.putExtra("net.sf.briar.LOCAL_AUTHOR_NAME", localAuthorName);
 		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
 		i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
 		i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
index 7094b77ed16faeb640bdbc89d63d6911ce75a6d9..0e1ffc63a6c05869145456f7188efcbfa8aaa5b0 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
@@ -23,23 +23,13 @@ import android.widget.TextView;
 
 class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> {
 
-	private final String contactName;
-
-	private String localAuthorName = null;
-
-	ConversationAdapter(Context ctx, String contactName) {
+	ConversationAdapter(Context ctx) {
 		super(ctx, android.R.layout.simple_expandable_list_item_1,
 				new ArrayList<PrivateMessageHeader>());
-		this.contactName = contactName;
-	}
-
-	void setLocalAuthorName(String localAuthorName) {
-		this.localAuthorName = localAuthorName;
 	}
 
 	@Override
 	public View getView(int position, View convertView, ViewGroup parent) {
-		if(localAuthorName == null) throw new IllegalStateException();
 		PrivateMessageHeader item = getItem(position);
 		Context ctx = getContext();
 
@@ -59,8 +49,7 @@ class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> {
 		name.setTextSize(18);
 		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
-		if(item.isIncoming()) name.setText(contactName);
-		else name.setText(localAuthorName);
+		name.setText(item.getAuthor().getName());
 		innerLayout.addView(name);
 
 		if(item.getContentType().equals("text/plain")) {
diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
index 722348bcf6310fe1e154755b5710e37cfe0b891d..bcd11a7e67570e47cb9ff8f54cc2504833ef3f36 100644
--- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
+++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
@@ -180,6 +180,10 @@ public interface DatabaseComponent {
 	/** Returns the group with the given ID, if the user subscribes to it. */
 	Group getGroup(GroupId g) throws DbException;
 
+	/** Returns the headers of all messages in the given group. */
+	Collection<GroupMessageHeader> getGroupMessageHeaders(GroupId g)
+			throws DbException;
+
 	/** Returns the pseudonym with the given ID. */
 	LocalAuthor getLocalAuthor(AuthorId a) throws DbException;
 
@@ -199,10 +203,6 @@ public interface DatabaseComponent {
 	/** Returns the body of the message with the given ID. */
 	byte[] getMessageBody(MessageId m) throws DbException;
 
-	/** Returns the headers of all messages in the given group. */
-	Collection<GroupMessageHeader> getMessageHeaders(GroupId g)
-			throws DbException;
-
 	/**
 	 * Returns the headers of all private messages to or from the given
 	 * contact.
diff --git a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java
index 7e6d98cee65318cda02311afb654ec5a7b0e4f8b..df6e4ac240449dced84cb06934ff5bada1260990 100644
--- a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java
+++ b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java
@@ -8,35 +8,17 @@ import net.sf.briar.api.messaging.Rating;
 public class GroupMessageHeader extends MessageHeader {
 
 	private final GroupId groupId;
-	private final Author author;
-	private final Rating rating;
 
-	public GroupMessageHeader(MessageId id, MessageId parent,
+	public GroupMessageHeader(MessageId id, MessageId parent, Author author,
 			String contentType, String subject, long timestamp, boolean read,
-			boolean starred, GroupId groupId, Author author, Rating rating) {
-		super(id, parent, contentType, subject, timestamp, read, starred);
+			boolean starred, Rating rating, GroupId groupId) {
+		super(id, parent, author, contentType, subject, timestamp, read,
+				starred, rating);
 		this.groupId = groupId;
-		this.author = author;
-		this.rating = rating;
 	}
 
 	/** Returns the ID of the group to which the message belongs. */
 	public GroupId getGroupId() {
 		return groupId;
 	}
-
-	/**
-	 * Returns the message's author, or null if this is an  anonymous message.
-	 */
-	public Author getAuthor() {
-		return author;
-	}
-
-	/**
-	 * Returns the rating for the message's author, or Rating.UNRATED if this
-	 * is an anonymous message.
-	 */
-	public Rating getRating() {
-		return rating;
-	}
 }
diff --git a/briar-api/src/net/sf/briar/api/db/MessageHeader.java b/briar-api/src/net/sf/briar/api/db/MessageHeader.java
index 95a927d3e7fdf8ae7334c1da297c16570cd8f414..2f48df5b6a639eec5749c8e9d1ea159a305ef56d 100644
--- a/briar-api/src/net/sf/briar/api/db/MessageHeader.java
+++ b/briar-api/src/net/sf/briar/api/db/MessageHeader.java
@@ -1,23 +1,30 @@
 package net.sf.briar.api.db;
 
+import net.sf.briar.api.Author;
 import net.sf.briar.api.messaging.MessageId;
+import net.sf.briar.api.messaging.Rating;
 
 public abstract class MessageHeader {
 
 	private final MessageId id, parent;
+	private final Author author;
 	private final String contentType, subject;
 	private final long timestamp;
 	private final boolean read, starred;
+	private final Rating rating;
 
-	protected MessageHeader(MessageId id, MessageId parent, String contentType,
-			String subject, long timestamp, boolean read, boolean starred) {
+	protected MessageHeader(MessageId id, MessageId parent, Author author,
+			String contentType, String subject, long timestamp, boolean read,
+			boolean starred, Rating rating) {
 		this.id = id;
 		this.parent = parent;
+		this.author = author;
 		this.contentType = contentType;
 		this.subject = subject;
 		this.timestamp = timestamp;
 		this.read = read;
 		this.starred = starred;
+		this.rating = rating;
 	}
 
 	/** Returns the message's unique identifier. */
@@ -33,6 +40,13 @@ public abstract class MessageHeader {
 		return parent;
 	}
 
+	/**
+	 * Returns the message's author, or null if this is an  anonymous message.
+	 */
+	public Author getAuthor() {
+		return author;
+	}
+
 	/** Returns the message's content type. */
 	public String getContentType() {
 		return contentType;
@@ -57,4 +71,12 @@ public abstract class MessageHeader {
 	public boolean isStarred() {
 		return starred;
 	}
+
+	/**
+	 * Returns the rating for the message's author, or Rating.UNRATED if this
+	 * is an anonymous message.
+	 */
+	public Rating getRating() {
+		return rating;
+	}
 }
diff --git a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java
index af55211c66fb691f233ce82816525d03fca2ff29..5a9b3c34728f093dfffb139c6f672282343d51b9 100644
--- a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java
+++ b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java
@@ -1,17 +1,21 @@
 package net.sf.briar.api.db;
 
+import net.sf.briar.api.Author;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.messaging.MessageId;
+import net.sf.briar.api.messaging.Rating;
 
 public class PrivateMessageHeader extends MessageHeader {
 
 	private final ContactId contactId;
 	private final boolean incoming;
 
-	public PrivateMessageHeader(MessageId id, MessageId parent,
+	public PrivateMessageHeader(MessageId id, MessageId parent, Author author,
 			String contentType, String subject, long timestamp, boolean read,
-			boolean starred, ContactId contactId, boolean incoming) {
-		super(id, parent, contentType, subject, timestamp, read, starred);
+			boolean starred, Rating rating, ContactId contactId,
+			boolean incoming) {
+		super(id, parent, author, contentType, subject, timestamp, read,
+				starred, rating);
 		this.contactId = contactId;
 		this.incoming = incoming;
 	}
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index f00b294a4ecf01b646387aba8878f263970c929e..499dd6c933b17569e8be9bcdf0f9b97b371813e5 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -101,7 +101,8 @@ interface Database<T> {
 	 * <p>
 	 * Locking: message write.
 	 */
-	boolean addGroupMessage(T txn, Message m) throws DbException;
+	boolean addGroupMessage(T txn, Message m, boolean incoming)
+			throws DbException;
 
 	/**
 	 * Stores a pseudonym that the user can use to sign messages.
@@ -131,7 +132,8 @@ interface Database<T> {
 	 * <p>
 	 * Locking: message write.
 	 */
-	boolean addPrivateMessage(T txn, Message m, ContactId c) throws DbException;
+	boolean addPrivateMessage(T txn, Message m, ContactId c, boolean incoming)
+			throws DbException;
 
 	/**
 	 * Stores the given temporary secrets and deletes any secrets that have
@@ -272,6 +274,14 @@ interface Database<T> {
 	 */
 	Group getGroup(T txn, GroupId g) throws DbException;
 
+	/**
+	 * Returns the headers of all messages in the given group.
+	 * <p>
+	 * Locking: message read, rating read.
+	 */
+	Collection<GroupMessageHeader> getGroupMessageHeaders(T txn, GroupId g)
+			throws DbException;
+
 	/**
 	 * Returns the parent of the given group message, or null if either the
 	 * message has no parent, or the parent is absent from the database, or the
@@ -333,19 +343,11 @@ interface Database<T> {
 	 */
 	byte[] getMessageBody(T txn, MessageId m) throws DbException;
 
-	/**
-	 * Returns the headers of all messages in the given group.
-	 * <p>
-	 * Locking: message read, rating read.
-	 */
-	Collection<GroupMessageHeader> getMessageHeaders(T txn, GroupId g)
-			throws DbException;
-
 	/**
 	 * Returns the headers of all private messages to or from the given
 	 * contact.
 	 * <p>
-	 * Locking: message read.
+	 * Locking: contact read, identity read, message read, rating read.
 	 */
 	Collection<PrivateMessageHeader> getPrivateMessageHeaders(T txn,
 			ContactId c) throws DbException;
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index fcaff944b87b5d53b20258a5ff8784ec91dca065..b37caf2079dd4809dcf25577d859a5c0372392e0 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -347,10 +347,9 @@ DatabaseCleaner.Callback {
 	 * Locking: contact read, message write, rating read.
 	 * @param sender is null for a locally generated message.
 	 */
-	private boolean storeGroupMessage(T txn, Message m, ContactId sender)
-			throws DbException {
+	private boolean storeGroupMessage(T txn, Message m, ContactId sender) throws DbException {
 		if(m.getGroup() == null) throw new IllegalArgumentException();
-		boolean stored = db.addGroupMessage(txn, m);
+		boolean stored = db.addGroupMessage(txn, m, sender != null);
 		if(stored && sender == null) db.setReadFlag(txn, m.getId(), true);
 		// Mark the message as seen by the sender
 		MessageId id = m.getId();
@@ -456,9 +455,9 @@ DatabaseCleaner.Callback {
 
 	/**
 	 * If the given message is already in the database, returns false.
-	 * Otherwise stores the message and marks it as new or seen with respect to
-	 * the given contact, depending on whether the message is outgoing or
-	 * incoming, respectively.
+	 * Otherwise stores the message and marks it as seen or unseen with respect
+	 * to the given contact, depending on whether the message is incoming or
+	 * outgoing, respectively.
 	 * <p>
 	 * Locking: message write.
 	 */
@@ -466,7 +465,7 @@ DatabaseCleaner.Callback {
 			boolean incoming) throws DbException {
 		if(m.getGroup() != null) throw new IllegalArgumentException();
 		if(m.getAuthor() != null) throw new IllegalArgumentException();
-		if(!db.addPrivateMessage(txn, m, c)) {
+		if(!db.addPrivateMessage(txn, m, c, incoming)) {
 			if(LOG.isLoggable(INFO))
 				LOG.info("Duplicate private message not stored");
 			return false;
@@ -984,6 +983,37 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public Collection<GroupMessageHeader> getGroupMessageHeaders(GroupId g)
+			throws DbException {
+		messageLock.readLock().lock();
+		try {
+			ratingLock.readLock().lock();
+			try {
+				subscriptionLock.readLock().lock();
+				try {
+					T txn = db.startTransaction();
+					try {
+						if(!db.containsSubscription(txn, g))
+							throw new NoSuchSubscriptionException();
+						Collection<GroupMessageHeader> headers =
+								db.getGroupMessageHeaders(txn, g);
+						db.commitTransaction(txn);
+						return headers;
+					} catch(DbException e) {
+						db.abortTransaction(txn);
+						throw e;
+					}
+				} finally {
+					subscriptionLock.readLock().unlock();
+				}
+			} finally {
+				ratingLock.readLock().unlock();
+			}
+		} finally {
+			messageLock.readLock().unlock();
+		}
+	}
+
 	public LocalAuthor getLocalAuthor(AuthorId a) throws DbException {
 		identityLock.readLock().lock();
 		try {
@@ -1093,53 +1123,37 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Collection<GroupMessageHeader> getMessageHeaders(GroupId g)
-			throws DbException {
-		messageLock.readLock().lock();
+	public Collection<PrivateMessageHeader> getPrivateMessageHeaders(
+			ContactId c) throws DbException {
+		contactLock.readLock().lock();
 		try {
-			ratingLock.readLock().lock();
+			identityLock.readLock().lock();
 			try {
-				subscriptionLock.readLock().lock();
+				messageLock.readLock().lock();
 				try {
-					T txn = db.startTransaction();
+					ratingLock.readLock().lock();
 					try {
-						if(!db.containsSubscription(txn, g))
-							throw new NoSuchSubscriptionException();
-						Collection<GroupMessageHeader> headers =
-								db.getMessageHeaders(txn, g);
-						db.commitTransaction(txn);
-						return headers;
-					} catch(DbException e) {
-						db.abortTransaction(txn);
-						throw e;
+						T txn = db.startTransaction();
+						try {
+							Collection<PrivateMessageHeader> headers =
+									db.getPrivateMessageHeaders(txn, c);
+							db.commitTransaction(txn);
+							return headers;
+						} catch(DbException e) {
+							db.abortTransaction(txn);
+							throw e;
+						}
+					} finally {
+						ratingLock.readLock().unlock();
 					}
 				} finally {
-					subscriptionLock.readLock().unlock();
+					messageLock.readLock().unlock();
 				}
 			} finally {
-				ratingLock.readLock().unlock();
-			}
-		} finally {
-			messageLock.readLock().unlock();
-		}
-	}
-
-	public Collection<PrivateMessageHeader> getPrivateMessageHeaders(
-			ContactId c) throws DbException {
-		messageLock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Collection<PrivateMessageHeader> headers =
-						db.getPrivateMessageHeaders(txn, c);
-				db.commitTransaction(txn);
-				return headers;
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
+				identityLock.readLock().unlock();
 			}
 		} finally {
-			messageLock.readLock().unlock();
+			contactLock.readLock().unlock();
 		}
 	}
 
@@ -1510,675 +1524,675 @@ DatabaseCleaner.Callback {
 	 * <p>
 	 * Locking: contact read, message write, rating read, subscription read.
 	 */
-	private boolean storeMessage(T txn, ContactId c, Message m)
-			throws DbException {
-		long now = clock.currentTimeMillis();
-		if(m.getTimestamp() > now + MAX_CLOCK_DIFFERENCE) {
-			if(LOG.isLoggable(INFO))
-				LOG.info("Discarding message with future timestamp");
-			return false;
-		}
-		Group g = m.getGroup();
-		if(g == null) return storePrivateMessage(txn, m, c, true);
-		if(!db.containsVisibleSubscription(txn, c, g.getId())) {
-			if(LOG.isLoggable(INFO))
-				LOG.info("Discarding message without visible subscription");
-			return false;
-		}
-		return storeGroupMessage(txn, m, c);
-	}
-
-	public Request receiveOffer(ContactId c, Offer o) throws DbException {
-		Collection<MessageId> offered;
-		BitSet request;
-		contactLock.readLock().lock();
-		try {
-			messageLock.writeLock().lock();
-			try {
-				subscriptionLock.readLock().lock();
-				try {
-					T txn = db.startTransaction();
-					try {
-						if(!db.containsContact(txn, c))
-							throw new NoSuchContactException();
-						offered = o.getMessageIds();
-						request = new BitSet(offered.size());
-						Iterator<MessageId> it = offered.iterator();
-						for(int i = 0; it.hasNext(); i++) {
-							// If the message is not in the database, or not
-							// visible to the contact, request it
-							MessageId m = it.next();
-							if(!db.setStatusSeenIfVisible(txn, c, m))
-								request.set(i);
-						}
-						db.commitTransaction(txn);
-					} catch(DbException e) {
-						db.abortTransaction(txn);
-						throw e;
-					}
-				} finally {
-					subscriptionLock.readLock().unlock();
-				}
-			} finally {
-				messageLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-		return new Request(request, offered.size());
-	}
-
-	public void receiveRetentionAck(ContactId c, RetentionAck a)
-			throws DbException {
-		contactLock.readLock().lock();
-		try {
-			retentionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					db.setRetentionUpdateAcked(txn, c, a.getVersion());
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				retentionLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	public void receiveRetentionUpdate(ContactId c, RetentionUpdate u)
-			throws DbException {
-		boolean updated;
-		contactLock.readLock().lock();
-		try {
-			retentionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					updated = db.setRetentionTime(txn, c, u.getRetentionTime(),
-							u.getVersion());
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				retentionLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-		if(updated) callListeners(new RemoteRetentionTimeUpdatedEvent(c));
-	}
-
-	public void receiveSubscriptionAck(ContactId c, SubscriptionAck a)
-			throws DbException {
-		contactLock.readLock().lock();
-		try {
-			subscriptionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					db.setSubscriptionUpdateAcked(txn, c, a.getVersion());
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				subscriptionLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate u)
-			throws DbException {
-		boolean updated;
-		contactLock.readLock().lock();
-		try {
-			subscriptionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					Collection<Group> groups = u.getGroups();
-					long version = u.getVersion();
-					updated = db.setSubscriptions(txn, c, groups, version);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				subscriptionLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-		if(updated) callListeners(new RemoteSubscriptionsUpdatedEvent(c));
-	}
-
-	public void receiveTransportAck(ContactId c, TransportAck a)
-			throws DbException {
-		contactLock.readLock().lock();
-		try {
-			transportLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					TransportId t = a.getId();
-					if(!db.containsTransport(txn, t))
-						throw new NoSuchTransportException();
-					db.setTransportUpdateAcked(txn, c, t, a.getVersion());
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				transportLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	public void receiveTransportUpdate(ContactId c, TransportUpdate u)
-			throws DbException {
-		boolean updated;
-		contactLock.readLock().lock();
-		try {
-			transportLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					TransportId t = u.getId();
-					TransportProperties p = u.getProperties();
-					long version = u.getVersion();
-					updated = db.setRemoteProperties(txn, c, t, p, version);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				transportLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-		if(updated)
-			callListeners(new RemoteTransportsUpdatedEvent(c, u.getId()));
-	}
-
-	public void removeContact(ContactId c) throws DbException {
-		contactLock.writeLock().lock();
-		try {
-			messageLock.writeLock().lock();
-			try {
-				retentionLock.writeLock().lock();
-				try {
-					subscriptionLock.writeLock().lock();
-					try {
-						transportLock.writeLock().lock();
-						try {
-							windowLock.writeLock().lock();
-							try {
-								T txn = db.startTransaction();
-								try {
-									if(!db.containsContact(txn, c))
-										throw new NoSuchContactException();
-									db.removeContact(txn, c);
-									db.commitTransaction(txn);
-								} catch(DbException e) {
-									db.abortTransaction(txn);
-									throw e;
-								}
-							} finally {
-								windowLock.writeLock().unlock();
-							}
-						} finally {
-							transportLock.writeLock().unlock();
-						}
-					} finally {
-						subscriptionLock.writeLock().unlock();
-					}
-				} finally {
-					retentionLock.writeLock().unlock();
-				}
-			} finally {
-				messageLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.writeLock().unlock();
-		}
-		callListeners(new ContactRemovedEvent(c));
-	}
-
-	public void removeTransport(TransportId t) throws DbException {
-		transportLock.writeLock().lock();
-		try {
-			windowLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsTransport(txn, t))
-						throw new NoSuchTransportException();
-					db.removeTransport(txn, t);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				windowLock.writeLock().unlock();
-			}
-		} finally {
-			transportLock.writeLock().unlock();
-		}
-		callListeners(new TransportRemovedEvent(t));
-	}
-
-	public void setConnectionWindow(ContactId c, TransportId t, long period,
-			long centre, byte[] bitmap) throws DbException {
-		contactLock.readLock().lock();
-		try {
-			transportLock.readLock().lock();
-			try {
-				windowLock.writeLock().lock();
-				try {
-					T txn = db.startTransaction();
-					try {
-						if(!db.containsContact(txn, c))
-							throw new NoSuchContactException();
-						if(!db.containsTransport(txn, t))
-							throw new NoSuchTransportException();
-						db.setConnectionWindow(txn, c, t, period, centre,
-								bitmap);
-						db.setLastConnected(txn, c, clock.currentTimeMillis());
-						db.commitTransaction(txn);
-					} catch(DbException e) {
-						db.abortTransaction(txn);
-						throw e;
-					}
-				} finally {
-					windowLock.writeLock().unlock();
-				}
-			} finally {
-				transportLock.readLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	public void setRating(AuthorId a, Rating r) throws DbException {
-		boolean changed;
-		messageLock.writeLock().lock();
-		try {
-			ratingLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					Rating old = db.setRating(txn, a, r);
-					changed = (old != r);
-					// Update the sendability of the author's messages
-					if(r == GOOD && old != GOOD)
-						updateAuthorSendability(txn, a, true);
-					else if(r != GOOD && old == GOOD)
-						updateAuthorSendability(txn, a, false);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				ratingLock.writeLock().unlock();
-			}
-		} finally {
-			messageLock.writeLock().unlock();
-		}
-		if(changed) callListeners(new RatingChangedEvent(a, r));
-	}
-
-	public boolean setReadFlag(MessageId m, boolean read) throws DbException {
-		messageLock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if(!db.containsMessage(txn, m))
-					throw new NoSuchMessageException();
-				boolean wasRead = db.setReadFlag(txn, m, read);
-				db.commitTransaction(txn);
-				return wasRead;
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			messageLock.writeLock().unlock();
-		}
-	}
-
-	public void setRemoteProperties(ContactId c,
-			Map<TransportId, TransportProperties> p) throws DbException {
-		contactLock.readLock().lock();
-		try {
-			transportLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsContact(txn, c))
-						throw new NoSuchContactException();
-					db.setRemoteProperties(txn, c, p);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				transportLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	public void setSeen(ContactId c, Collection<MessageId> seen)
-			throws DbException {
-		contactLock.readLock().lock();
-		try {
-			messageLock.writeLock().lock();
-			try {
-				subscriptionLock.readLock().lock();
-				try {
-					T txn = db.startTransaction();
-					try {
-						if(!db.containsContact(txn, c))
-							throw new NoSuchContactException();
-						for(MessageId m : seen)
-							db.setStatusSeenIfVisible(txn, c, m);
-						db.commitTransaction(txn);
-					} catch(DbException e) {
-						db.abortTransaction(txn);
-						throw e;
-					}
-				} finally {
-					subscriptionLock.readLock().unlock();
-				}
-			} finally {
-				messageLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	/**
-	 * Updates the sendability of all messages written by the given author, and
-	 * the ancestors of those messages if necessary.
-	 * <p>
-	 * Locking: message write.
-	 * @param increment true if the user's rating for the author has changed
-	 * from not good to good, or false if it has changed from good to not good.
-	 */
-	private void updateAuthorSendability(T txn, AuthorId a, boolean increment)
-			throws DbException {
-		for(MessageId id : db.getMessagesByAuthor(txn, a)) {
-			int sendability = db.getSendability(txn, id);
-			if(increment) {
-				db.setSendability(txn, id, sendability + 1);
-				if(sendability == 0)
-					updateAncestorSendability(txn, id, true);
-			} else {
-				assert sendability > 0;
-				db.setSendability(txn, id, sendability - 1);
-				if(sendability == 1)
-					updateAncestorSendability(txn, id, false);
-			}
-		}
-	}
-
-	public boolean setStarredFlag(MessageId m, boolean starred)
-			throws DbException {
-		messageLock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if(!db.containsMessage(txn, m))
-					throw new NoSuchMessageException();
-				boolean wasStarred = db.setStarredFlag(txn, m, starred);
-				db.commitTransaction(txn);
-				return wasStarred;
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			messageLock.writeLock().unlock();
-		}
-	}
-
-	public void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException {
-		Collection<ContactId> affected = new ArrayList<ContactId>();
-		contactLock.readLock().lock();
-		try {
-			subscriptionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsSubscription(txn, g))
-						throw new NoSuchSubscriptionException();
-					// Use HashSets for O(1) lookups, O(n) overall running time
-					HashSet<ContactId> now = new HashSet<ContactId>(visible);
-					Collection<ContactId> before = db.getVisibility(txn, g);
-					before = new HashSet<ContactId>(before);
-					// Set the group's visibility for each current contact
-					for(ContactId c : db.getContactIds(txn)) {
-						boolean wasBefore = before.contains(c);
-						boolean isNow = now.contains(c);
-						if(!wasBefore && isNow) {
-							db.addVisibility(txn, c, g);
-							affected.add(c);
-						} else if(wasBefore && !isNow) {
-							db.removeVisibility(txn, c, g);
-							affected.add(c);
-						}
-					}
-					// Make the group invisible to future contacts
-					db.setVisibleToAll(txn, g, false);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				subscriptionLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-		if(!affected.isEmpty())
-			callListeners(new LocalSubscriptionsUpdatedEvent(affected));
-	}
-
-	public void setVisibleToAll(GroupId g, boolean visible) throws DbException {
-		Collection<ContactId> affected = new ArrayList<ContactId>();
-		contactLock.readLock().lock();
-		try {
-			subscriptionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					if(!db.containsSubscription(txn, g))
-						throw new NoSuchSubscriptionException();
-					// Make the group visible or invisible to future contacts
-					db.setVisibleToAll(txn, g, visible);
-					if(visible) {
-						// Make the group visible to all current contacts
-						Collection<ContactId> before = db.getVisibility(txn, g);
-						before = new HashSet<ContactId>(before);
-						for(ContactId c : db.getContactIds(txn)) {
-							if(!before.contains(c)) {
-								db.addVisibility(txn, c, g);
-								affected.add(c);
-							}
-						}
-					}
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				subscriptionLock.writeLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-		if(!affected.isEmpty())
-			callListeners(new LocalSubscriptionsUpdatedEvent(affected));
-	}
-
-	public boolean subscribe(Group g) throws DbException {
-		boolean added = false;
-		subscriptionLock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if(!db.containsSubscription(txn, g.getId()))
-					added = db.addSubscription(txn, g);
-				db.commitTransaction(txn);
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			subscriptionLock.writeLock().unlock();
-		}
-		if(added) callListeners(new SubscriptionAddedEvent(g));
-		return added;
-	}
-
-	public void unsubscribe(Group g) throws DbException {
-		Collection<ContactId> affected;
-		messageLock.writeLock().lock();
-		try {
-			subscriptionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					GroupId id = g.getId();
-					if(!db.containsSubscription(txn, id))
-						throw new NoSuchSubscriptionException();
-					affected = db.getVisibility(txn, id);
-					db.removeSubscription(txn, id);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				subscriptionLock.writeLock().unlock();
-			}
-		} finally {
-			messageLock.writeLock().unlock();
-		}
-		callListeners(new SubscriptionRemovedEvent(g));
-		callListeners(new LocalSubscriptionsUpdatedEvent(affected));
-	}
-
-	public void checkFreeSpaceAndClean() throws DbException {
-		long freeSpace = db.getFreeSpace();
-		if(LOG.isLoggable(INFO)) LOG.info(freeSpace + " bytes free space");
-		while(freeSpace < MIN_FREE_SPACE) {
-			boolean expired = expireMessages(BYTES_PER_SWEEP);
-			if(freeSpace < CRITICAL_FREE_SPACE && !expired) {
-				// FIXME: Work out what to do here
-				throw new Error("Disk space is critically low");
-			}
-			Thread.yield();
-			freeSpace = db.getFreeSpace();
-		}
-	}
-
-	/**
-	 * Removes the oldest messages from the database, with a total size less
-	 * than or equal to the given size, and returns true if any messages were
-	 * removed.
-	 */
-	private boolean expireMessages(int size) throws DbException {
-		Collection<MessageId> expired;
-		messageLock.writeLock().lock();
-		try {
-			retentionLock.writeLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					expired = db.getOldMessages(txn, size);
-					if(!expired.isEmpty()) {
-						for(MessageId m : expired) removeMessage(txn, m);
-						db.incrementRetentionVersions(txn);
-						if(LOG.isLoggable(INFO))
-							LOG.info("Expired " + expired.size() + " messages");
-					}
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				retentionLock.writeLock().unlock();
-			}
-		} finally {
-			messageLock.writeLock().unlock();
-		}
-		if(expired.isEmpty()) return false;
-		callListeners(new MessageExpiredEvent());
-		return true;
-	}
-
-	/**
-	 * Removes the given message (and all associated state) from the database.
-	 * <p>
-	 * Locking: message write.
-	 */
-	private void removeMessage(T txn, MessageId m) throws DbException {
-		int sendability = db.getSendability(txn, m);
-		// If the message is sendable, deleting it may affect its ancestors'
-		// sendability (backward inclusion)
-		if(sendability > 0) updateAncestorSendability(txn, m, false);
-		db.removeMessage(txn, m);
-	}
-
-	public boolean shouldCheckFreeSpace() {
-		synchronized(spaceLock) {
-			long now = clock.currentTimeMillis();
-			if(bytesStoredSinceLastCheck > MAX_BYTES_BETWEEN_SPACE_CHECKS
-					|| now - timeOfLastCheck > MAX_MS_BETWEEN_SPACE_CHECKS) {
-				bytesStoredSinceLastCheck = 0;
-				timeOfLastCheck = now;
-				return true;
-			}
-		}
-		return false;
-	}
+	 private boolean storeMessage(T txn, ContactId c, Message m)
+			 throws DbException {
+		 long now = clock.currentTimeMillis();
+		 if(m.getTimestamp() > now + MAX_CLOCK_DIFFERENCE) {
+			 if(LOG.isLoggable(INFO))
+				 LOG.info("Discarding message with future timestamp");
+			 return false;
+		 }
+		 Group g = m.getGroup();
+		 if(g == null) return storePrivateMessage(txn, m, c, true);
+		 if(!db.containsVisibleSubscription(txn, c, g.getId())) {
+			 if(LOG.isLoggable(INFO))
+				 LOG.info("Discarding message without visible subscription");
+			 return false;
+		 }
+		 return storeGroupMessage(txn, m, c);
+	 }
+
+	 public Request receiveOffer(ContactId c, Offer o) throws DbException {
+		 Collection<MessageId> offered;
+		 BitSet request;
+		 contactLock.readLock().lock();
+		 try {
+			 messageLock.writeLock().lock();
+			 try {
+				 subscriptionLock.readLock().lock();
+				 try {
+					 T txn = db.startTransaction();
+					 try {
+						 if(!db.containsContact(txn, c))
+							 throw new NoSuchContactException();
+						 offered = o.getMessageIds();
+						 request = new BitSet(offered.size());
+						 Iterator<MessageId> it = offered.iterator();
+						 for(int i = 0; it.hasNext(); i++) {
+							 // If the message is not in the database, or not
+							 // visible to the contact, request it
+							 MessageId m = it.next();
+							 if(!db.setStatusSeenIfVisible(txn, c, m))
+								 request.set(i);
+						 }
+						 db.commitTransaction(txn);
+					 } catch(DbException e) {
+						 db.abortTransaction(txn);
+						 throw e;
+					 }
+				 } finally {
+					 subscriptionLock.readLock().unlock();
+				 }
+			 } finally {
+				 messageLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+		 return new Request(request, offered.size());
+	 }
+
+	 public void receiveRetentionAck(ContactId c, RetentionAck a)
+			 throws DbException {
+		 contactLock.readLock().lock();
+		 try {
+			 retentionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 db.setRetentionUpdateAcked(txn, c, a.getVersion());
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 retentionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+	 }
+
+	 public void receiveRetentionUpdate(ContactId c, RetentionUpdate u)
+			 throws DbException {
+		 boolean updated;
+		 contactLock.readLock().lock();
+		 try {
+			 retentionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 updated = db.setRetentionTime(txn, c, u.getRetentionTime(),
+							 u.getVersion());
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 retentionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+		 if(updated) callListeners(new RemoteRetentionTimeUpdatedEvent(c));
+	 }
+
+	 public void receiveSubscriptionAck(ContactId c, SubscriptionAck a)
+			 throws DbException {
+		 contactLock.readLock().lock();
+		 try {
+			 subscriptionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 db.setSubscriptionUpdateAcked(txn, c, a.getVersion());
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 subscriptionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+	 }
+
+	 public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate u)
+			 throws DbException {
+		 boolean updated;
+		 contactLock.readLock().lock();
+		 try {
+			 subscriptionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 Collection<Group> groups = u.getGroups();
+					 long version = u.getVersion();
+					 updated = db.setSubscriptions(txn, c, groups, version);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 subscriptionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+		 if(updated) callListeners(new RemoteSubscriptionsUpdatedEvent(c));
+	 }
+
+	 public void receiveTransportAck(ContactId c, TransportAck a)
+			 throws DbException {
+		 contactLock.readLock().lock();
+		 try {
+			 transportLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 TransportId t = a.getId();
+					 if(!db.containsTransport(txn, t))
+						 throw new NoSuchTransportException();
+					 db.setTransportUpdateAcked(txn, c, t, a.getVersion());
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 transportLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+	 }
+
+	 public void receiveTransportUpdate(ContactId c, TransportUpdate u)
+			 throws DbException {
+		 boolean updated;
+		 contactLock.readLock().lock();
+		 try {
+			 transportLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 TransportId t = u.getId();
+					 TransportProperties p = u.getProperties();
+					 long version = u.getVersion();
+					 updated = db.setRemoteProperties(txn, c, t, p, version);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 transportLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+		 if(updated)
+			 callListeners(new RemoteTransportsUpdatedEvent(c, u.getId()));
+	 }
+
+	 public void removeContact(ContactId c) throws DbException {
+		 contactLock.writeLock().lock();
+		 try {
+			 messageLock.writeLock().lock();
+			 try {
+				 retentionLock.writeLock().lock();
+				 try {
+					 subscriptionLock.writeLock().lock();
+					 try {
+						 transportLock.writeLock().lock();
+						 try {
+							 windowLock.writeLock().lock();
+							 try {
+								 T txn = db.startTransaction();
+								 try {
+									 if(!db.containsContact(txn, c))
+										 throw new NoSuchContactException();
+									 db.removeContact(txn, c);
+									 db.commitTransaction(txn);
+								 } catch(DbException e) {
+									 db.abortTransaction(txn);
+									 throw e;
+								 }
+							 } finally {
+								 windowLock.writeLock().unlock();
+							 }
+						 } finally {
+							 transportLock.writeLock().unlock();
+						 }
+					 } finally {
+						 subscriptionLock.writeLock().unlock();
+					 }
+				 } finally {
+					 retentionLock.writeLock().unlock();
+				 }
+			 } finally {
+				 messageLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.writeLock().unlock();
+		 }
+		 callListeners(new ContactRemovedEvent(c));
+	 }
+
+	 public void removeTransport(TransportId t) throws DbException {
+		 transportLock.writeLock().lock();
+		 try {
+			 windowLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsTransport(txn, t))
+						 throw new NoSuchTransportException();
+					 db.removeTransport(txn, t);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 windowLock.writeLock().unlock();
+			 }
+		 } finally {
+			 transportLock.writeLock().unlock();
+		 }
+		 callListeners(new TransportRemovedEvent(t));
+	 }
+
+	 public void setConnectionWindow(ContactId c, TransportId t, long period,
+			 long centre, byte[] bitmap) throws DbException {
+		 contactLock.readLock().lock();
+		 try {
+			 transportLock.readLock().lock();
+			 try {
+				 windowLock.writeLock().lock();
+				 try {
+					 T txn = db.startTransaction();
+					 try {
+						 if(!db.containsContact(txn, c))
+							 throw new NoSuchContactException();
+						 if(!db.containsTransport(txn, t))
+							 throw new NoSuchTransportException();
+						 db.setConnectionWindow(txn, c, t, period, centre,
+								 bitmap);
+						 db.setLastConnected(txn, c, clock.currentTimeMillis());
+						 db.commitTransaction(txn);
+					 } catch(DbException e) {
+						 db.abortTransaction(txn);
+						 throw e;
+					 }
+				 } finally {
+					 windowLock.writeLock().unlock();
+				 }
+			 } finally {
+				 transportLock.readLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+	 }
+
+	 public void setRating(AuthorId a, Rating r) throws DbException {
+		 boolean changed;
+		 messageLock.writeLock().lock();
+		 try {
+			 ratingLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 Rating old = db.setRating(txn, a, r);
+					 changed = (old != r);
+					 // Update the sendability of the author's messages
+					 if(r == GOOD && old != GOOD)
+						 updateAuthorSendability(txn, a, true);
+					 else if(r != GOOD && old == GOOD)
+						 updateAuthorSendability(txn, a, false);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 ratingLock.writeLock().unlock();
+			 }
+		 } finally {
+			 messageLock.writeLock().unlock();
+		 }
+		 if(changed) callListeners(new RatingChangedEvent(a, r));
+	 }
+
+	 public boolean setReadFlag(MessageId m, boolean read) throws DbException {
+		 messageLock.writeLock().lock();
+		 try {
+			 T txn = db.startTransaction();
+			 try {
+				 if(!db.containsMessage(txn, m))
+					 throw new NoSuchMessageException();
+				 boolean wasRead = db.setReadFlag(txn, m, read);
+				 db.commitTransaction(txn);
+				 return wasRead;
+			 } catch(DbException e) {
+				 db.abortTransaction(txn);
+				 throw e;
+			 }
+		 } finally {
+			 messageLock.writeLock().unlock();
+		 }
+	 }
+
+	 public void setRemoteProperties(ContactId c,
+			 Map<TransportId, TransportProperties> p) throws DbException {
+		 contactLock.readLock().lock();
+		 try {
+			 transportLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsContact(txn, c))
+						 throw new NoSuchContactException();
+					 db.setRemoteProperties(txn, c, p);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 transportLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+	 }
+
+	 public void setSeen(ContactId c, Collection<MessageId> seen)
+			 throws DbException {
+		 contactLock.readLock().lock();
+		 try {
+			 messageLock.writeLock().lock();
+			 try {
+				 subscriptionLock.readLock().lock();
+				 try {
+					 T txn = db.startTransaction();
+					 try {
+						 if(!db.containsContact(txn, c))
+							 throw new NoSuchContactException();
+						 for(MessageId m : seen)
+							 db.setStatusSeenIfVisible(txn, c, m);
+						 db.commitTransaction(txn);
+					 } catch(DbException e) {
+						 db.abortTransaction(txn);
+						 throw e;
+					 }
+				 } finally {
+					 subscriptionLock.readLock().unlock();
+				 }
+			 } finally {
+				 messageLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+	 }
+
+	 /**
+	  * Updates the sendability of all messages written by the given author, and
+	  * the ancestors of those messages if necessary.
+	  * <p>
+	  * Locking: message write.
+	  * @param increment true if the user's rating for the author has changed
+	  * from not good to good, or false if it has changed from good to not good.
+	  */
+	 private void updateAuthorSendability(T txn, AuthorId a, boolean increment)
+			 throws DbException {
+		 for(MessageId id : db.getMessagesByAuthor(txn, a)) {
+			 int sendability = db.getSendability(txn, id);
+			 if(increment) {
+				 db.setSendability(txn, id, sendability + 1);
+				 if(sendability == 0)
+					 updateAncestorSendability(txn, id, true);
+			 } else {
+				 assert sendability > 0;
+				 db.setSendability(txn, id, sendability - 1);
+				 if(sendability == 1)
+					 updateAncestorSendability(txn, id, false);
+			 }
+		 }
+	 }
+
+	 public boolean setStarredFlag(MessageId m, boolean starred)
+			 throws DbException {
+		 messageLock.writeLock().lock();
+		 try {
+			 T txn = db.startTransaction();
+			 try {
+				 if(!db.containsMessage(txn, m))
+					 throw new NoSuchMessageException();
+				 boolean wasStarred = db.setStarredFlag(txn, m, starred);
+				 db.commitTransaction(txn);
+				 return wasStarred;
+			 } catch(DbException e) {
+				 db.abortTransaction(txn);
+				 throw e;
+			 }
+		 } finally {
+			 messageLock.writeLock().unlock();
+		 }
+	 }
+
+	 public void setVisibility(GroupId g, Collection<ContactId> visible)
+			 throws DbException {
+		 Collection<ContactId> affected = new ArrayList<ContactId>();
+		 contactLock.readLock().lock();
+		 try {
+			 subscriptionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsSubscription(txn, g))
+						 throw new NoSuchSubscriptionException();
+					 // Use HashSets for O(1) lookups, O(n) overall running time
+					 HashSet<ContactId> now = new HashSet<ContactId>(visible);
+					 Collection<ContactId> before = db.getVisibility(txn, g);
+					 before = new HashSet<ContactId>(before);
+					 // Set the group's visibility for each current contact
+					 for(ContactId c : db.getContactIds(txn)) {
+						 boolean wasBefore = before.contains(c);
+						 boolean isNow = now.contains(c);
+						 if(!wasBefore && isNow) {
+							 db.addVisibility(txn, c, g);
+							 affected.add(c);
+						 } else if(wasBefore && !isNow) {
+							 db.removeVisibility(txn, c, g);
+							 affected.add(c);
+						 }
+					 }
+					 // Make the group invisible to future contacts
+					 db.setVisibleToAll(txn, g, false);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 subscriptionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+		 if(!affected.isEmpty())
+			 callListeners(new LocalSubscriptionsUpdatedEvent(affected));
+	 }
+
+	 public void setVisibleToAll(GroupId g, boolean visible) throws DbException {
+		 Collection<ContactId> affected = new ArrayList<ContactId>();
+		 contactLock.readLock().lock();
+		 try {
+			 subscriptionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 if(!db.containsSubscription(txn, g))
+						 throw new NoSuchSubscriptionException();
+					 // Make the group visible or invisible to future contacts
+					 db.setVisibleToAll(txn, g, visible);
+					 if(visible) {
+						 // Make the group visible to all current contacts
+						 Collection<ContactId> before = db.getVisibility(txn, g);
+						 before = new HashSet<ContactId>(before);
+						 for(ContactId c : db.getContactIds(txn)) {
+							 if(!before.contains(c)) {
+								 db.addVisibility(txn, c, g);
+								 affected.add(c);
+							 }
+						 }
+					 }
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 subscriptionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 contactLock.readLock().unlock();
+		 }
+		 if(!affected.isEmpty())
+			 callListeners(new LocalSubscriptionsUpdatedEvent(affected));
+	 }
+
+	 public boolean subscribe(Group g) throws DbException {
+		 boolean added = false;
+		 subscriptionLock.writeLock().lock();
+		 try {
+			 T txn = db.startTransaction();
+			 try {
+				 if(!db.containsSubscription(txn, g.getId()))
+					 added = db.addSubscription(txn, g);
+				 db.commitTransaction(txn);
+			 } catch(DbException e) {
+				 db.abortTransaction(txn);
+				 throw e;
+			 }
+		 } finally {
+			 subscriptionLock.writeLock().unlock();
+		 }
+		 if(added) callListeners(new SubscriptionAddedEvent(g));
+		 return added;
+	 }
+
+	 public void unsubscribe(Group g) throws DbException {
+		 Collection<ContactId> affected;
+		 messageLock.writeLock().lock();
+		 try {
+			 subscriptionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 GroupId id = g.getId();
+					 if(!db.containsSubscription(txn, id))
+						 throw new NoSuchSubscriptionException();
+					 affected = db.getVisibility(txn, id);
+					 db.removeSubscription(txn, id);
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 subscriptionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 messageLock.writeLock().unlock();
+		 }
+		 callListeners(new SubscriptionRemovedEvent(g));
+		 callListeners(new LocalSubscriptionsUpdatedEvent(affected));
+	 }
+
+	 public void checkFreeSpaceAndClean() throws DbException {
+		 long freeSpace = db.getFreeSpace();
+		 if(LOG.isLoggable(INFO)) LOG.info(freeSpace + " bytes free space");
+		 while(freeSpace < MIN_FREE_SPACE) {
+			 boolean expired = expireMessages(BYTES_PER_SWEEP);
+			 if(freeSpace < CRITICAL_FREE_SPACE && !expired) {
+				 // FIXME: Work out what to do here
+				 throw new Error("Disk space is critically low");
+			 }
+			 Thread.yield();
+			 freeSpace = db.getFreeSpace();
+		 }
+	 }
+
+	 /**
+	  * Removes the oldest messages from the database, with a total size less
+	  * than or equal to the given size, and returns true if any messages were
+	  * removed.
+	  */
+	 private boolean expireMessages(int size) throws DbException {
+		 Collection<MessageId> expired;
+		 messageLock.writeLock().lock();
+		 try {
+			 retentionLock.writeLock().lock();
+			 try {
+				 T txn = db.startTransaction();
+				 try {
+					 expired = db.getOldMessages(txn, size);
+					 if(!expired.isEmpty()) {
+						 for(MessageId m : expired) removeMessage(txn, m);
+						 db.incrementRetentionVersions(txn);
+						 if(LOG.isLoggable(INFO))
+							 LOG.info("Expired " + expired.size() + " messages");
+					 }
+					 db.commitTransaction(txn);
+				 } catch(DbException e) {
+					 db.abortTransaction(txn);
+					 throw e;
+				 }
+			 } finally {
+				 retentionLock.writeLock().unlock();
+			 }
+		 } finally {
+			 messageLock.writeLock().unlock();
+		 }
+		 if(expired.isEmpty()) return false;
+		 callListeners(new MessageExpiredEvent());
+		 return true;
+	 }
+
+	 /**
+	  * Removes the given message (and all associated state) from the database.
+	  * <p>
+	  * Locking: message write.
+	  */
+	 private void removeMessage(T txn, MessageId m) throws DbException {
+		 int sendability = db.getSendability(txn, m);
+		 // If the message is sendable, deleting it may affect its ancestors'
+		 // sendability (backward inclusion)
+		 if(sendability > 0) updateAncestorSendability(txn, m, false);
+		 db.removeMessage(txn, m);
+	 }
+
+	 public boolean shouldCheckFreeSpace() {
+		 synchronized(spaceLock) {
+			 long now = clock.currentTimeMillis();
+			 if(bytesStoredSinceLastCheck > MAX_BYTES_BETWEEN_SPACE_CHECKS
+					 || now - timeOfLastCheck > MAX_MS_BETWEEN_SPACE_CHECKS) {
+				 bytesStoredSinceLastCheck = 0;
+				 timeOfLastCheck = now;
+				 return true;
+			 }
+		 }
+		 return false;
+	 }
 }
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index 797ea9a56b6a759a88f337241cae256075d6bf57..1578e7d3887583cf823ad6c585691c186858c0f3 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -161,6 +161,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " bodyStart INT NOT NULL,"
 					+ " bodyLength INT NOT NULL,"
 					+ " raw BLOB NOT NULL,"
+					+ " incoming BOOLEAN NOT NULL,"
 					+ " sendability INT UNSIGNED," // Null for private messages
 					+ " contactId INT UNSIGNED," // Null for group messages
 					+ " read BOOLEAN NOT NULL,"
@@ -685,7 +686,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean addGroupMessage(Connection txn, Message m)
+	public boolean addGroupMessage(Connection txn, Message m, boolean incoming)
 			throws DbException {
 		if(m.getGroup() == null) throw new IllegalArgumentException();
 		if(containsMessage(txn, m.getId())) return false;
@@ -694,9 +695,9 @@ abstract class JdbcDatabase implements Database<Connection> {
 			String sql = "INSERT INTO messages (messageId, parentId, groupId,"
 					+ " authorId, authorName, authorKey, contentType, subject,"
 					+ " timestamp, length, bodyStart, bodyLength, raw,"
-					+ " sendability, read, starred)"
-					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ZERO(),"
-					+ " FALSE, FALSE)";
+					+ " incoming, sendability, read, starred)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,"
+					+ " ZERO(), FALSE, FALSE)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getId().getBytes());
 			if(m.getParent() == null) ps.setNull(2, BINARY);
@@ -720,6 +721,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setInt(11, m.getBodyStart());
 			ps.setInt(12, m.getBodyLength());
 			ps.setBytes(13, raw);
+			ps.setBoolean(14, incoming);
 			int affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
@@ -803,16 +805,17 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean addPrivateMessage(Connection txn, Message m, ContactId c)
-			throws DbException {
+	public boolean addPrivateMessage(Connection txn, Message m, ContactId c,
+			boolean incoming) throws DbException {
 		if(m.getGroup() != null) throw new IllegalArgumentException();
+		if(m.getAuthor() != null) throw new IllegalArgumentException();
 		if(containsMessage(txn, m.getId())) return false;
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO messages (messageId, parentId,"
 					+ " contentType, subject, timestamp, length, bodyStart,"
-					+ " bodyLength, raw, contactId, read, starred)"
-					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, FALSE)";
+					+ " bodyLength, raw, incoming, contactId, read, starred)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, FALSE)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getId().getBytes());
 			if(m.getParent() == null) ps.setNull(2, BINARY);
@@ -825,7 +828,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setInt(7, m.getBodyStart());
 			ps.setInt(8, m.getBodyLength());
 			ps.setBytes(9, raw);
-			ps.setInt(10, c.getInt());
+			ps.setBoolean(10, incoming);
+			ps.setInt(11, c.getInt());
 			int affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
@@ -1333,6 +1337,60 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Collection<GroupMessageHeader> getGroupMessageHeaders(Connection txn,
+			GroupId g) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT messageId, parentId, m.authorId, authorName,"
+					+ " authorKey, rating, contentType, subject, timestamp,"
+					+ " read, starred"
+					+ " FROM messages AS m"
+					+ " LEFT OUTER JOIN ratings AS r"
+					+ " ON m.authorId = r.authorId"
+					+ " WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			List<GroupMessageHeader> headers =
+					new ArrayList<GroupMessageHeader>();
+			while(rs.next()) {
+				MessageId id = new MessageId(rs.getBytes(1));
+				byte[] b = rs.getBytes(2);
+				MessageId parent = b == null ? null : new MessageId(b);
+				Author author;
+				Rating rating;
+				b = rs.getBytes(3);
+				if(b == null) {
+					author = null;
+					rating = UNRATED;
+				} else {
+					AuthorId authorId = new AuthorId(b);
+					String authorName = rs.getString(4);
+					byte[] authorKey = rs.getBytes(5);
+					author = new Author(authorId, authorName, authorKey);
+					// NULL == 0 == UNRATED
+					rating = Rating.values()[rs.getByte(6)];
+				}
+				String contentType = rs.getString(7);
+				String subject = rs.getString(8);
+				long timestamp = rs.getLong(9);
+				boolean read = rs.getBoolean(10);
+				boolean starred = rs.getBoolean(11);
+				headers.add(new GroupMessageHeader(id, parent, author,
+						contentType, subject, timestamp, read, starred, rating,
+						g));
+			}
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableList(headers);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public MessageId getGroupMessageParent(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1546,59 +1604,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<GroupMessageHeader> getMessageHeaders(Connection txn,
-			GroupId g) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT messageId, parentId, m.authorId, authorName,"
-					+ " authorKey, rating, contentType, subject, timestamp,"
-					+ " read, starred"
-					+ " FROM messages AS m"
-					+ " LEFT OUTER JOIN ratings AS r"
-					+ " ON m.authorId = r.authorId"
-					+ " WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			rs = ps.executeQuery();
-			List<GroupMessageHeader> headers =
-					new ArrayList<GroupMessageHeader>();
-			while(rs.next()) {
-				MessageId id = new MessageId(rs.getBytes(1));
-				byte[] b = rs.getBytes(2);
-				MessageId parent = b == null ? null : new MessageId(b);
-				Author author;
-				Rating rating;
-				b = rs.getBytes(3);
-				if(b == null) {
-					author = null;
-					rating = UNRATED;
-				} else {
-					AuthorId authorId = new AuthorId(b);
-					String authorName = rs.getString(4);
-					byte[] authorKey = rs.getBytes(5);
-					author = new Author(authorId, authorName, authorKey);
-					// NULL == 0 == UNRATED
-					rating = Rating.values()[rs.getByte(6)];
-				}
-				String contentType = rs.getString(7);
-				String subject = rs.getString(8);
-				long timestamp = rs.getLong(9);
-				boolean read = rs.getBoolean(10);
-				boolean starred = rs.getBoolean(11);
-				headers.add(new GroupMessageHeader(id, parent, contentType,
-						subject, timestamp, read, starred, g, author, rating));
-			}
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(headers);
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Collection<MessageId> getMessagesByAuthor(Connection txn, AuthorId a)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1767,13 +1772,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
+			// Get the incoming message headers
 			String sql = "SELECT m.messageId, parentId, contentType, subject,"
-					+ " timestamp, read, starred, seen"
+					+ " timestamp, read, starred, c.authorId, name, publicKey,"
+					+ " rating"
 					+ " FROM messages AS m"
-					+ " JOIN statuses AS s"
-					+ " ON m.messageId = s.messageId"
-					+ " AND m.contactId = s.contactId"
-					+ " WHERE m.contactId = ? AND groupId IS NULL";
+					+ " JOIN contacts AS c"
+					+ " ON m.contactId = c.contactId"
+					+ " LEFT OUTER JOIN ratings AS r"
+					+ " ON c.authorId = r.authorId"
+					+ " WHERE m.contactId = ?"
+					+ " AND groupId IS NULL"
+					+ " AND incoming = TRUE";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			rs = ps.executeQuery();
@@ -1788,9 +1798,53 @@ abstract class JdbcDatabase implements Database<Connection> {
 				long timestamp = rs.getLong(5);
 				boolean read = rs.getBoolean(6);
 				boolean starred = rs.getBoolean(7);
-				boolean seen = rs.getBoolean(8);
-				headers.add(new PrivateMessageHeader(id, parent, contentType,
-						subject, timestamp, read, starred, c, seen));
+				AuthorId authorId = new AuthorId(rs.getBytes(8));
+				String authorName = rs.getString(9);
+				byte[] authorKey = rs.getBytes(10);
+				Author author = new Author(authorId, authorName, authorKey);
+				// NULL == 0 == UNRATED
+				Rating rating = Rating.values()[rs.getByte(11)];
+				headers.add(new PrivateMessageHeader(id, parent, author,
+						contentType, subject, timestamp, read, starred, rating,
+						c, true));
+			}
+			rs.close();
+			ps.close();
+			// Get the outgoing message headers
+			sql = "SELECT m.messageId, parentId, contentType, subject,"
+					+ " timestamp, read, starred, a.authorId, a.name,"
+					+ " a.publicKey, rating"
+					+ " FROM messages AS m"
+					+ " JOIN contacts AS c"
+					+ " ON m.contactId = c.contactId"
+					+ " JOIN localAuthors AS a"
+					+ " ON c.localAuthorId = a.authorId"
+					+ " LEFT OUTER JOIN ratings AS r"
+					+ " ON c.localAuthorId = r.authorId"
+					+ " WHERE m.contactId = ?"
+					+ " AND groupId IS NULL"
+					+ " AND incoming = FALSE";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			rs = ps.executeQuery();
+			while(rs.next()) {
+				MessageId id = new MessageId(rs.getBytes(1));
+				byte[] b = rs.getBytes(2);
+				MessageId parent = b == null ? null : new MessageId(b);
+				String contentType = rs.getString(3);
+				String subject = rs.getString(4);
+				long timestamp = rs.getLong(5);
+				boolean read = rs.getBoolean(6);
+				boolean starred = rs.getBoolean(7);
+				AuthorId authorId = new AuthorId(rs.getBytes(8));
+				String authorName = rs.getString(9);
+				byte[] authorKey = rs.getBytes(10);
+				Author author = new Author(authorId, authorName, authorKey);
+				// NULL == 0 == UNRATED
+				Rating rating = Rating.values()[rs.getByte(11)];
+				headers.add(new PrivateMessageHeader(id, parent, author,
+						contentType, subject, timestamp, read, starred, rating,
+						c, false));
 			}
 			rs.close();
 			ps.close();
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index 84d3a10d6f5eaa125a57f404cf2a68366f4fc4e6..fbb0fdda958dabf8f584f491f6b23a9ec1b272fa 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -172,7 +172,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			// getMessageHeaders(groupId)
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).getMessageHeaders(txn, groupId);
+			oneOf(database).getGroupMessageHeaders(txn, groupId);
 			will(returnValue(Collections.emptyList()));
 			// getSubscriptions()
 			oneOf(database).getSubscriptions(txn);
@@ -212,7 +212,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				db.getRemoteProperties(transportId));
 		db.subscribe(group); // First time - listeners called
 		db.subscribe(group); // Second time - not called
-		assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId));
+		assertEquals(Collections.emptyList(),
+				db.getGroupMessageHeaders(groupId));
 		assertEquals(Arrays.asList(groupId), db.getSubscriptions());
 		db.unsubscribe(group);
 		db.removeContact(contactId);
@@ -367,7 +368,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, false);
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 		}});
@@ -392,7 +393,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, false);
 			will(returnValue(true));
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).getContactIds(txn);
@@ -428,7 +429,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, false);
 			will(returnValue(true));
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).getContactIds(txn);
@@ -467,7 +468,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// addLocalPrivateMessage(privateMessage, contactId)
-			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
+			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
+					false);
 			will(returnValue(false));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
@@ -492,7 +494,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// addLocalPrivateMessage(privateMessage, contactId)
-			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
+			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
+					false);
 			will(returnValue(true));
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).addStatus(txn, contactId, messageId, false);
@@ -702,7 +705,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		} catch(NoSuchSubscriptionException expected) {}
 
 		try {
-			db.getMessageHeaders(groupId);
+			db.getGroupMessageHeaders(groupId);
 			fail();
 		} catch(NoSuchSubscriptionException expected) {}
 
@@ -1148,7 +1151,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// The message is stored
-			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
+			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
+					true);
 			will(returnValue(true));
 			oneOf(database).addStatus(txn, contactId, messageId, true);
 			// The message must be acked
@@ -1175,8 +1179,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).commitTransaction(txn);
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			// The message is stored, but it's a duplicate
-			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
+			// The message is not stored, it's a duplicate
+			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
+					true);
 			will(returnValue(false));
 			// The message must still be acked
 			oneOf(database).addMessageToAck(txn, contactId, messageId);
@@ -1236,8 +1241,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsVisibleSubscription(txn, contactId,
 					groupId);
 			will(returnValue(true));
-			// The message is stored, but it's a duplicate
-			oneOf(database).addGroupMessage(txn, message);
+			// The message is not stored, it's a duplicate
+			oneOf(database).addGroupMessage(txn, message, true);
 			will(returnValue(false));
 			oneOf(database).addStatus(txn, contactId, messageId, true);
 			// The message must be acked
@@ -1269,7 +1274,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 					groupId);
 			will(returnValue(true));
 			// The message is stored, and it's not a duplicate
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, true);
 			will(returnValue(true));
 			oneOf(database).addStatus(txn, contactId, messageId, true);
 			// Set the status to seen = true for all other contacts (none)
@@ -1311,7 +1316,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 					groupId);
 			will(returnValue(true));
 			// The message is stored, and it's not a duplicate
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, true);
 			will(returnValue(true));
 			oneOf(database).addStatus(txn, contactId, messageId, true);
 			// Set the status to seen = true for all other contacts (none)
@@ -1513,7 +1518,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, false);
 			will(returnValue(true));
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).getContactIds(txn);
@@ -1553,7 +1558,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// addLocalPrivateMessage(privateMessage, contactId)
-			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
+			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
+					false);
 			will(returnValue(true));
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).addStatus(txn, contactId, messageId, false);
@@ -1585,7 +1591,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).addGroupMessage(txn, message);
+			oneOf(database).addGroupMessage(txn, message, false);
 			will(returnValue(false));
 			oneOf(database).commitTransaction(txn);
 			// The message was not added, so the listener should not be called
@@ -1615,7 +1621,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			// addLocalPrivateMessage(privateMessage, contactId)
-			oneOf(database).addPrivateMessage(txn, privateMessage, contactId);
+			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
+					false);
 			will(returnValue(false));
 			// The message was not added, so the listener should not be called
 		}});
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index aa0f778a3ddf9b14f9d982cfb8d4f23105a6eb74..b2c8e45be3cd07e25887016b4bf9dfaa5dbd595a 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -110,10 +110,10 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		assertTrue(db.containsSubscription(txn, groupId));
 		assertFalse(db.containsMessage(txn, messageId));
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, true);
 		assertTrue(db.containsMessage(txn, messageId));
 		assertFalse(db.containsMessage(txn, messageId1));
-		db.addPrivateMessage(txn, privateMessage, contactId);
+		db.addPrivateMessage(txn, privateMessage, contactId, true);
 		assertTrue(db.containsMessage(txn, messageId1));
 		db.commitTransaction(txn);
 		db.close();
@@ -173,7 +173,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to a group and store a message
 		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// Unsubscribing from the group should remove the message
 		assertTrue(db.containsMessage(txn, messageId));
@@ -192,7 +192,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact and store a private message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addPrivateMessage(txn, privateMessage, contactId);
+		db.addPrivateMessage(txn, privateMessage, contactId, false);
 
 		// Removing the contact should remove the message
 		assertTrue(db.containsMessage(txn, messageId1));
@@ -212,7 +212,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact and store a private message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addPrivateMessage(txn, privateMessage, contactId);
+		db.addPrivateMessage(txn, privateMessage, contactId, false);
 
 		// The message has no status yet, so it should not be sendable
 		assertFalse(db.hasSendableMessages(txn, contactId));
@@ -241,7 +241,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact and store a private message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addPrivateMessage(txn, privateMessage, contactId);
+		db.addPrivateMessage(txn, privateMessage, contactId, false);
 		db.addStatus(txn, contactId, messageId1, false);
 
 		// The message is sendable, but too large to send
@@ -273,7 +273,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.addStatus(txn, contactId, messageId, false);
 
 		// The message should not be sendable
@@ -312,7 +312,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.setSendability(txn, messageId, 1);
 
 		// The message has no status yet, so it should not be sendable
@@ -349,7 +349,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.setSendability(txn, messageId, 1);
 		db.addStatus(txn, contactId, messageId, false);
 
@@ -388,7 +388,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.setSendability(txn, messageId, 1);
 		db.addStatus(txn, contactId, messageId, false);
 
@@ -419,7 +419,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addSubscription(txn, group);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.setSendability(txn, messageId, 1);
 		db.addStatus(txn, contactId, messageId, false);
 
@@ -505,7 +505,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.setSendability(txn, messageId, 1);
 		db.addStatus(txn, contactId, messageId, false);
 
@@ -545,8 +545,8 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to a group and store two messages
 		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
-		db.addGroupMessage(txn, message1);
+		db.addGroupMessage(txn, message, false);
+		db.addGroupMessage(txn, message1, false);
 
 		// Check that each message is retrievable via its author
 		Iterator<MessageId> it =
@@ -583,10 +583,10 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Subscribe to the groups and store the messages
 		db.addSubscription(txn, group);
 		db.addSubscription(txn, group1);
-		db.addGroupMessage(txn, message);
-		db.addGroupMessage(txn, child1);
-		db.addGroupMessage(txn, child2);
-		db.addGroupMessage(txn, child3);
+		db.addGroupMessage(txn, message, false);
+		db.addGroupMessage(txn, child1, false);
+		db.addGroupMessage(txn, child2, false);
+		db.addGroupMessage(txn, child3, false);
 		// Make all the children sendable
 		db.setSendability(txn, childId1, 1);
 		db.setSendability(txn, childId2, 5);
@@ -613,8 +613,8 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to a group and store two messages
 		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
-		db.addGroupMessage(txn, message1);
+		db.addGroupMessage(txn, message, false);
+		db.addGroupMessage(txn, message1, false);
 
 		// Allowing enough capacity for one message should return the older one
 		Iterator<MessageId> it = db.getOldMessages(txn, size).iterator();
@@ -653,7 +653,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Storing a message should reduce the free space
 		Connection txn = db.startTransaction();
 		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message1);
+		db.addGroupMessage(txn, message1, false);
 		db.commitTransaction(txn);
 		assertTrue(db.getFreeSpace() < free);
 
@@ -894,7 +894,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// Set the sendability to > 0 and the status to seen = true
 		db.setSendability(txn, messageId, 1);
@@ -919,7 +919,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// Set the sendability to 0 and the status to seen = false
 		db.setSendability(txn, messageId, 0);
@@ -945,7 +945,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
 		db.setRetentionTime(txn, contactId, timestamp + 1, 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// Set the sendability to > 0 and the status to seen = false
 		db.setSendability(txn, messageId, 1);
@@ -969,7 +969,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// Set the sendability to > 0 and the status to seen = false
 		db.setSendability(txn, messageId, 1);
@@ -1032,7 +1032,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.addStatus(txn, contactId, messageId, false);
 
 		// There's no contact subscription for the group
@@ -1053,7 +1053,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addSubscription(txn, group);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		db.addStatus(txn, contactId, messageId, false);
 
 		// The subscription is not visible
@@ -1075,7 +1075,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// The message has already been seen by the contact
 		db.addStatus(txn, contactId, messageId, true);
@@ -1098,7 +1098,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// The message has not been seen by the contact
 		db.addStatus(txn, contactId, messageId, false);
@@ -1146,7 +1146,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		MessageId childId = new MessageId(TestUtils.getRandomId());
 		Message child = new TestMessage(childId, null, group, null, contentType,
 				subject, timestamp, raw);
-		db.addGroupMessage(txn, child);
+		db.addGroupMessage(txn, child, false);
 		assertTrue(db.containsMessage(txn, childId));
 		assertNull(db.getGroupMessageParent(txn, childId));
 
@@ -1167,7 +1167,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		MessageId parentId = new MessageId(TestUtils.getRandomId());
 		Message child = new TestMessage(childId, parentId, group, null,
 				contentType, subject, timestamp, raw);
-		db.addGroupMessage(txn, child);
+		db.addGroupMessage(txn, child, false);
 		assertTrue(db.containsMessage(txn, childId));
 		assertFalse(db.containsMessage(txn, parentId));
 		assertNull(db.getGroupMessageParent(txn, childId));
@@ -1195,8 +1195,8 @@ public class H2DatabaseTest extends BriarTestCase {
 				contentType, subject, timestamp, raw);
 		Message parent = new TestMessage(parentId, null, group1, null,
 				contentType, subject, timestamp, raw);
-		db.addGroupMessage(txn, child);
-		db.addGroupMessage(txn, parent);
+		db.addGroupMessage(txn, child, false);
+		db.addGroupMessage(txn, parent, false);
 		assertTrue(db.containsMessage(txn, childId));
 		assertTrue(db.containsMessage(txn, parentId));
 		assertNull(db.getGroupMessageParent(txn, childId));
@@ -1219,8 +1219,8 @@ public class H2DatabaseTest extends BriarTestCase {
 		MessageId childId = new MessageId(TestUtils.getRandomId());
 		Message child = new TestMessage(childId, messageId1, group, null,
 				contentType, subject, timestamp, raw);
-		db.addGroupMessage(txn, child);
-		db.addPrivateMessage(txn, privateMessage, contactId);
+		db.addGroupMessage(txn, child, false);
+		db.addPrivateMessage(txn, privateMessage, contactId, false);
 		assertTrue(db.containsMessage(txn, childId));
 		assertTrue(db.containsMessage(txn, messageId1));
 		assertNull(db.getGroupMessageParent(txn, childId));
@@ -1245,8 +1245,8 @@ public class H2DatabaseTest extends BriarTestCase {
 				contentType, subject, timestamp, raw);
 		Message parent = new TestMessage(parentId, null, group, null,
 				contentType, subject, timestamp, raw);
-		db.addGroupMessage(txn, child);
-		db.addGroupMessage(txn, parent);
+		db.addGroupMessage(txn, child, false);
+		db.addGroupMessage(txn, parent, false);
 		assertTrue(db.containsMessage(txn, childId));
 		assertTrue(db.containsMessage(txn, parentId));
 		assertEquals(parentId, db.getGroupMessageParent(txn, childId));
@@ -1271,8 +1271,8 @@ public class H2DatabaseTest extends BriarTestCase {
 				contentType, subject, timestamp, raw, 5, bodyLength);
 		Message privateMessage1 = new TestMessage(messageId1, null, null,
 				null, contentType, subject, timestamp, raw, 10, bodyLength);
-		db.addGroupMessage(txn, message1);
-		db.addPrivateMessage(txn, privateMessage1, contactId);
+		db.addGroupMessage(txn, message1, false);
+		db.addPrivateMessage(txn, privateMessage1, contactId, false);
 
 		// Calculate the expected message bodies
 		byte[] expectedBody = new byte[bodyLength];
@@ -1305,19 +1305,19 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group);
 
 		// Store a couple of messages
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		MessageId parentId = new MessageId(TestUtils.getRandomId());
 		long timestamp1 = System.currentTimeMillis();
 		Message message1 = new TestMessage(messageId1, parentId, group, author,
 				contentType, subject, timestamp1, raw);
-		db.addGroupMessage(txn, message1);
+		db.addGroupMessage(txn, message1, false);
 		// Mark one of the messages read
 		assertFalse(db.setReadFlag(txn, messageId, true));
 
 		// Retrieve the message headers
 		Collection<GroupMessageHeader> headers =
-				db.getMessageHeaders(txn, groupId);
+				db.getGroupMessageHeaders(txn, groupId);
 		Iterator<GroupMessageHeader> it = headers.iterator();
 		boolean messageFound = false, message1Found = false;
 		// First header (order is undefined)
@@ -1380,7 +1380,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to a group and store a message
 		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// The message should be unread by default
 		assertFalse(db.getReadFlag(txn, messageId));
@@ -1406,7 +1406,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to a group and store a message
 		db.addSubscription(txn, group);
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 
 		// The message should be unstarred by default
 		assertFalse(db.getStarredFlag(txn, messageId));
@@ -1437,17 +1437,17 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, group1);
 
 		// Store two messages in the first group
-		db.addGroupMessage(txn, message);
+		db.addGroupMessage(txn, message, false);
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		Message message1 = new TestMessage(messageId1, null, group, author,
 				contentType, subject, timestamp, raw);
-		db.addGroupMessage(txn, message1);
+		db.addGroupMessage(txn, message1, false);
 
 		// Store one message in the second group
 		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
 		Message message2 = new TestMessage(messageId2, null, group1, author,
 				contentType, subject, timestamp, raw);
-		db.addGroupMessage(txn, message2);
+		db.addGroupMessage(txn, message2, false);
 
 		// Mark one of the messages in the first group read
 		assertFalse(db.setReadFlag(txn, messageId, true));