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 79076e317469c5396f648f46dc67ad1f87fe51d6..4e8095e9a3e8fd0fd920f25c27dec586438123f0 100644
--- a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
@@ -234,7 +234,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
 		Author author = item.getAuthor();
 		if(author != null) {
-			i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
 			i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
 			i.putExtra("net.sf.briar.RATING", item.getRating().toString());
 		}
diff --git a/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java b/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
index 416fd13c92f2623fc40bdf04d79f0ad6daeda495..f1cf62ac8f2ee80a044ef7ae11eca466f6fe2a1b 100644
--- a/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
@@ -24,7 +24,6 @@ import net.sf.briar.android.BriarService;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
 import net.sf.briar.android.widgets.HorizontalBorder;
 import net.sf.briar.android.widgets.HorizontalSpace;
-import net.sf.briar.api.AuthorId;
 import net.sf.briar.api.android.BundleEncrypter;
 import net.sf.briar.api.android.DatabaseUiExecutor;
 import net.sf.briar.api.db.DatabaseComponent;
@@ -66,8 +65,7 @@ implements OnClickListener {
 	private Rating rating = UNRATED;
 	private boolean read;
 	private ImageView thumb = null;
-	private ImageButton goodButton = null, badButton = null, readButton = null;
-	private ImageButton prevButton = null, nextButton = null;
+	private ImageButton readButton = null, prevButton = null, nextButton = null;
 	private ImageButton replyButton = null;
 	private TextView content = null;
 
@@ -75,7 +73,6 @@ implements OnClickListener {
 	@Inject private volatile DatabaseComponent db;
 	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
 	private volatile MessageId messageId = null;
-	private volatile AuthorId authorId = null;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -92,15 +89,9 @@ implements OnClickListener {
 		b = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
 		if(b == null) throw new IllegalStateException();
 		messageId = new MessageId(b);
-		String authorName = null;
-		b = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
-		if(b != null) {
-			authorId = new AuthorId(b);
-			authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
-			if(authorName == null) throw new IllegalStateException();
-			String r = i.getStringExtra("net.sf.briar.RATING");
-			if(r != null) rating = Rating.valueOf(r);
-		}
+		String authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
+		String r = i.getStringExtra("net.sf.briar.RATING");
+		if(r != null) rating = Rating.valueOf(r);
 		String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE");
 		if(contentType == null) throw new IllegalStateException();
 		long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
@@ -177,23 +168,6 @@ implements OnClickListener {
 		footer.setOrientation(HORIZONTAL);
 		footer.setGravity(CENTER);
 
-		goodButton = new ImageButton(this);
-		goodButton.setBackgroundResource(0);
-		goodButton.setImageResource(R.drawable.rating_good);
-		if(authorName == null) goodButton.setEnabled(false);
-		else goodButton.setOnClickListener(this);
-		footer.addView(goodButton);
-		footer.addView(new HorizontalSpace(this));
-
-		badButton = new ImageButton(this);
-		badButton.setBackgroundResource(0);
-		badButton.setImageResource(R.drawable.rating_bad);
-		badButton.setOnClickListener(this);
-		if(authorName == null) badButton.setEnabled(false);
-		else badButton.setOnClickListener(this);
-		footer.addView(badButton);
-		footer.addView(new HorizontalSpace(this));
-
 		readButton = new ImageButton(this);
 		readButton.setBackgroundResource(0);
 		if(read) readButton.setImageResource(R.drawable.content_unread);
@@ -309,13 +283,7 @@ implements OnClickListener {
 	}
 
 	public void onClick(View view) {
-		if(view == goodButton) {
-			if(rating == BAD) setRatingInDatabase(UNRATED);
-			else if(rating == UNRATED) setRatingInDatabase(GOOD);
-		} else if(view == badButton) {
-			if(rating == GOOD) setRatingInDatabase(UNRATED);
-			else if(rating == UNRATED) setRatingInDatabase(BAD);
-		} else if(view == readButton) {
+		if(view == readButton) {
 			setReadInDatabase(!read);
 		} else if(view == prevButton) {
 			setResult(RESULT_PREV);
@@ -337,38 +305,4 @@ implements OnClickListener {
 			}
 		}
 	}
-
-	private void setRatingInDatabase(final Rating r) {
-		dbUiExecutor.execute(new Runnable() {
-			public void run() {
-				try {
-					serviceConnection.waitForStartup();
-					long now = System.currentTimeMillis();
-					db.setRating(authorId, r);
-					long duration = System.currentTimeMillis() - now;
-					if(LOG.isLoggable(INFO))
-						LOG.info("Setting rating took " + duration + " ms");
-					setRatingInUi(r);
-				} catch(DbException e) {
-					if(LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				} catch(InterruptedException e) {
-					if(LOG.isLoggable(INFO))
-						LOG.info("Interrupted while waiting for service");
-					Thread.currentThread().interrupt();
-				}
-			}
-		});
-	}
-
-	private void setRatingInUi(final Rating r) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				rating = r;
-				if(r == GOOD) thumb.setImageResource(R.drawable.rating_good);
-				else if(r == BAD) thumb.setImageResource(R.drawable.rating_bad);
-				else thumb.setImageResource(R.drawable.rating_unrated);
-			}
-		});
-	}
 }
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 499dd6c933b17569e8be9bcdf0f9b97b371813e5..6d24c05584a6941e7aeb7a185a52eca28a9126b4 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -352,14 +352,6 @@ interface Database<T> {
 	Collection<PrivateMessageHeader> getPrivateMessageHeaders(T txn,
 			ContactId c) throws DbException;
 
-	/**
-	 * Returns the IDs of all messages signed by the given author.
-	 * <p>
-	 * Locking: message read.
-	 */
-	Collection<MessageId> getMessagesByAuthor(T txn, AuthorId a)
-			throws DbException;
-
 	/**
 	 * Returns the IDs of some messages received from the given contact that
 	 * need to be acknowledged, up to the given number of messages.
@@ -554,6 +546,15 @@ interface Database<T> {
 	 */
 	Map<GroupId, Integer> getUnreadMessageCounts(T txn) throws DbException;
 
+	/**
+	 * Returns the IDs of all messages posted by the given author to
+	 * unrestricted groups.
+	 * <p>
+	 * Locking: message read.
+	 */
+	Collection<MessageId> getUnrestrictedGroupMessages(T txn, AuthorId a)
+			throws DbException;
+
 	/**
 	 * Returns the contacts to which the given group is visible.
 	 * <p>
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index b37caf2079dd4809dcf25577d859a5c0372392e0..3432df3801cac25b3a8b175615b461ab8a816b24 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -347,7 +347,8 @@ 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, sender != null);
 		if(stored && sender == null) db.setReadFlag(txn, m.getId(), true);
@@ -360,9 +361,13 @@ DatabaseCleaner.Callback {
 				if(!c.equals(sender)) db.addStatus(txn, c, id, false);
 			}
 			// Calculate and store the message's sendability
-			int sendability = calculateSendability(txn, m);
-			db.setSendability(txn, id, sendability);
-			if(sendability > 0) updateAncestorSendability(txn, id, true);
+			if(m.getGroup().isRestricted()) {
+				db.setSendability(txn, id, 1);
+			} else {
+				int sendability = calculateSendability(txn, m);
+				db.setSendability(txn, id, sendability);
+				if(sendability > 0) updateAncestorSendability(txn, id, true);
+			}
 			// Count the bytes stored
 			synchronized(spaceLock) {
 				bytesStoredSinceLastCheck += m.getSerialised().length;
@@ -1524,675 +1529,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 posted by the given author to
+	 * unrestricted groups, 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.getUnrestrictedGroupMessages(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 c54831de2d998ac6713036d312920bc5374a0d34..caf7cec3da826fa97fc7ed41d6a6ae40852be450 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -1601,27 +1601,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<MessageId> getMessagesByAuthor(Connection txn, AuthorId a)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT messageId FROM messages WHERE authorId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, a.getBytes());
-			rs = ps.executeQuery();
-			List<MessageId> ids = new ArrayList<MessageId>();
-			while(rs.next()) ids.add(new MessageId(rs.getBytes(1)));
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(ids);
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c,
 			int maxMessages) throws DbException {
 		PreparedStatement ps = null;
@@ -2568,6 +2547,32 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Collection<MessageId> getUnrestrictedGroupMessages(Connection txn,
+			AuthorId a) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT messageId"
+					+ " FROM messages AS m"
+					+ " JOIN groups AS g"
+					+ " ON m.groupId = g.groupId"
+					+ " WHERE authorId = ?"
+					+ " AND publicKey IS NULL";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, a.getBytes());
+			rs = ps.executeQuery();
+			List<MessageId> ids = new ArrayList<MessageId>();
+			while(rs.next()) ids.add(new MessageId(rs.getBytes(1)));
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableList(ids);
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<ContactId> getVisibility(Connection txn, GroupId g)
 			throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index fbb0fdda958dabf8f584f491f6b23a9ec1b272fa..a06a3c81ee25a5d91401452c67d8a13dc636616d 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -59,13 +59,13 @@ import org.junit.Test;
 public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	protected final Object txn = new Object();
-	protected final GroupId groupId;
-	protected final Group group;
+	protected final GroupId groupId, restrictedGroupId;
+	protected final Group group, restrictedGroup;
 	protected final AuthorId authorId;
 	protected final Author author;
 	protected final AuthorId localAuthorId;
 	protected final LocalAuthor localAuthor;
-	protected final MessageId messageId, messageId1;
+	protected final MessageId messageId, messageId1, privateMessageId;
 	protected final String contentType, subject;
 	protected final long timestamp;
 	protected final int size;
@@ -81,7 +81,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	public DatabaseComponentTest() {
 		super();
 		groupId = new GroupId(TestUtils.getRandomId());
+		restrictedGroupId = new GroupId(TestUtils.getRandomId());
 		group = new Group(groupId, "Group name", null);
+		restrictedGroup = new Group(restrictedGroupId, "Restricted group name",
+				new byte[60]);
 		authorId = new AuthorId(TestUtils.getRandomId());
 		author = new Author(authorId, "Alice", new byte[60]);
 		localAuthorId = new AuthorId(TestUtils.getRandomId());
@@ -89,6 +92,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				new byte[60]);
 		messageId = new MessageId(TestUtils.getRandomId());
 		messageId1 = new MessageId(TestUtils.getRandomId());
+		privateMessageId = new MessageId(TestUtils.getRandomId());
 		contentType = "text/plain";
 		subject = "Foo";
 		timestamp = System.currentTimeMillis();
@@ -96,7 +100,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		raw = new byte[size];
 		message = new TestMessage(messageId, null, group, author, contentType,
 				subject, timestamp, raw);
-		privateMessage = new TestMessage(messageId, null, null, null,
+		privateMessage = new TestMessage(privateMessageId, null, null, null,
 				contentType, subject, timestamp, raw);
 		transportId = new TransportId(TestUtils.getRandomId());
 		transportProperties = new TransportProperties(Collections.singletonMap(
@@ -139,7 +143,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			// setRating(authorId, GOOD)
 			oneOf(database).setRating(txn, authorId, GOOD);
 			will(returnValue(UNRATED));
-			oneOf(database).getMessagesByAuthor(txn, authorId);
+			oneOf(database).getUnrestrictedGroupMessages(txn, authorId);
 			will(returnValue(Collections.emptyList()));
 			oneOf(listener).eventOccurred(with(any(RatingChangedEvent.class)));
 			// setRating(authorId, GOOD) again
@@ -223,6 +227,59 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
+	@Test
+	public void testRestrictedGroupMessagesAreAlwaysSendable()
+			throws Exception {
+		final Message groupMessage = new TestMessage(messageId, null,
+				restrictedGroup, author, contentType, subject, timestamp, raw);
+		final Message groupMessage1 = new TestMessage(messageId1, null,
+				restrictedGroup, null, contentType, subject, timestamp, raw);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
+		context.checking(new Expectations() {{
+			// addLocalGroupMessage(groupMessage)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsSubscription(txn, restrictedGroupId);
+			will(returnValue(true));
+			oneOf(database).addGroupMessage(txn, groupMessage, false);
+			will(returnValue(true));
+			oneOf(database).setReadFlag(txn, messageId, true);
+			oneOf(database).getContactIds(txn);
+			will(returnValue(Arrays.asList(contactId)));
+			oneOf(database).addStatus(txn, contactId, messageId, false);
+			oneOf(database).setSendability(txn, messageId, 1);
+			oneOf(database).commitTransaction(txn);
+			// receiveMessage(groupMessage1)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			oneOf(database).containsVisibleSubscription(txn, contactId,
+					restrictedGroupId);
+			will(returnValue(true));
+			oneOf(database).addGroupMessage(txn, groupMessage1, true);
+			will(returnValue(true));
+			oneOf(database).addStatus(txn, contactId, messageId1, true);
+			oneOf(database).getContactIds(txn);
+			will(returnValue(Arrays.asList(contactId)));
+			oneOf(database).setSendability(txn, messageId1, 1);
+			oneOf(database).addMessageToAck(txn, contactId, messageId1);
+			oneOf(database).commitTransaction(txn);
+		}});
+
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				shutdown);
+
+		db.addLocalGroupMessage(groupMessage);
+		db.receiveMessage(contactId, groupMessage1);
+
+		context.assertIsSatisfied();
+	}
+	
 	@Test
 	public void testNullParentStopsBackwardInclusion() throws Exception {
 		Mockery context = new Mockery();
@@ -237,7 +294,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setRating(txn, authorId, GOOD);
 			will(returnValue(UNRATED));
 			// The sendability of the author's messages should be incremented
-			oneOf(database).getMessagesByAuthor(txn, authorId);
+			oneOf(database).getUnrestrictedGroupMessages(txn, authorId);
 			will(returnValue(Arrays.asList(messageId)));
 			oneOf(database).getSendability(txn, messageId);
 			will(returnValue(0));
@@ -269,7 +326,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setRating(txn, authorId, GOOD);
 			will(returnValue(UNRATED));
 			// The sendability of the author's messages should be incremented
-			oneOf(database).getMessagesByAuthor(txn, authorId);
+			oneOf(database).getUnrestrictedGroupMessages(txn, authorId);
 			will(returnValue(Arrays.asList(messageId)));
 			oneOf(database).getSendability(txn, messageId);
 			will(returnValue(0));
@@ -306,7 +363,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setRating(txn, authorId, GOOD);
 			will(returnValue(UNRATED));
 			// The sendability of the author's messages should be incremented
-			oneOf(database).getMessagesByAuthor(txn, authorId);
+			oneOf(database).getUnrestrictedGroupMessages(txn, authorId);
 			will(returnValue(Arrays.asList(messageId)));
 			oneOf(database).getSendability(txn, messageId);
 			will(returnValue(0));
@@ -497,8 +554,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
 					false);
 			will(returnValue(true));
-			oneOf(database).setReadFlag(txn, messageId, true);
-			oneOf(database).addStatus(txn, contactId, messageId, false);
+			oneOf(database).setReadFlag(txn, privateMessageId, true);
+			oneOf(database).addStatus(txn, contactId, privateMessageId, false);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown);
@@ -1154,9 +1211,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
 					true);
 			will(returnValue(true));
-			oneOf(database).addStatus(txn, contactId, messageId, true);
+			oneOf(database).addStatus(txn, contactId, privateMessageId, true);
 			// The message must be acked
-			oneOf(database).addMessageToAck(txn, contactId, messageId);
+			oneOf(database).addMessageToAck(txn, contactId, privateMessageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown);
@@ -1184,7 +1241,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 					true);
 			will(returnValue(false));
 			// The message must still be acked
-			oneOf(database).addMessageToAck(txn, contactId, messageId);
+			oneOf(database).addMessageToAck(txn, contactId, privateMessageId);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown);
@@ -1561,8 +1618,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).addPrivateMessage(txn, privateMessage, contactId,
 					false);
 			will(returnValue(true));
-			oneOf(database).setReadFlag(txn, messageId, true);
-			oneOf(database).addStatus(txn, contactId, messageId, false);
+			oneOf(database).setReadFlag(txn, privateMessageId, true);
+			oneOf(database).addStatus(txn, contactId, privateMessageId, false);
 			// The message was added, so the listener should be called
 			oneOf(listener).eventOccurred(with(any(
 					PrivateMessageAddedEvent.class)));
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index b2c8e45be3cd07e25887016b4bf9dfaa5dbd595a..65b53ff1be634d71f11e5c20fbc2ecb2c49d82b5 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -534,27 +534,39 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testGetMessagesByAuthor() throws Exception {
+	public void testGetUnrestrictedGroupMessages() throws Exception {
 		AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
 		Author author1 = new Author(authorId1, "Bob", new byte[60]);
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
 		Message message1 = new TestMessage(messageId1, null, group, author1,
 				contentType, subject, timestamp, raw);
+		GroupId groupId1 = new GroupId(TestUtils.getRandomId());
+		Group group1 = new Group(groupId1, "Restricted group name",
+				new byte[60]);
+		MessageId messageId2 = new MessageId(TestUtils.getRandomId());
+		Message message2 = new TestMessage(messageId2, null, group1, author,
+				contentType, subject, timestamp, raw);
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Subscribe to a group and store two messages
+		// Subscribe to an unrestricted group and store two messages
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message, false);
 		db.addGroupMessage(txn, message1, false);
 
-		// Check that each message is retrievable via its author
-		Iterator<MessageId> it =
-				db.getMessagesByAuthor(txn, authorId).iterator();
+		// Subscribe to a restricted group and store a message
+		db.addSubscription(txn, group1);
+		db.addGroupMessage(txn, message2, false);
+
+		// Check that only the messages in the unrestricted group are retrieved
+		Collection<MessageId> ids = db.getUnrestrictedGroupMessages(txn,
+				authorId);
+		Iterator<MessageId> it = ids.iterator();
 		assertTrue(it.hasNext());
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
-		it = db.getMessagesByAuthor(txn, authorId1).iterator();
+		ids = db.getUnrestrictedGroupMessages(txn, authorId1);
+		it = ids.iterator();
 		assertTrue(it.hasNext());
 		assertEquals(messageId1, it.next());
 		assertFalse(it.hasNext());