diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java
index 977df1292b67fd7d38e7cc88c3fd197d03a6e205..b6a980b67b83635f9eef97eca80a5ef3d67a6232 100644
--- a/components/net/sf/briar/db/Database.java
+++ b/components/net/sf/briar/db/Database.java
@@ -32,6 +32,7 @@ import net.sf.briar.api.transport.ConnectionWindow;
  * <ul>
  * <li> contact
  * <li> message
+ * <li> messageFlag
  * <li> messageStatus
  * <li> rating
  * <li> subscription
@@ -304,6 +305,13 @@ interface Database<T> {
 	 */
 	Rating getRating(T txn, AuthorId a) throws DbException;
 
+	/**
+	 * Returns true if the given message has been read.
+	 * <p>
+	 * Locking: message read, messageFlag read.
+	 */
+	boolean getRead(T txn, MessageId m) throws DbException;
+
 	/**
 	 * Returns all remote properties for the given transport.
 	 * <p>
@@ -346,6 +354,13 @@ interface Database<T> {
 	 */
 	byte[] getSharedSecret(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Returns true if the given message has been starred.
+	 * <p>
+	 * Locking: message read, messageFlag read.
+	 */
+	boolean getStarred(T txn, MessageId m) throws DbException;
+
 	/**
 	 * Returns the groups to which the user subscribes.
 	 * <p>
@@ -433,8 +448,8 @@ interface Database<T> {
 	/**
 	 * Removes a contact (and all associated state) from the database.
 	 * <p>
-	 * Locking: contact write, message write, messageStatus write,
-	 * subscription write, transport write.
+	 * Locking: contact write, message write, messageFlag write,
+	 * messageStatus write, subscription write, transport write.
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
@@ -450,7 +465,8 @@ interface Database<T> {
 	/**
 	 * Removes a message (and all associated state) from the database.
 	 * <p>
-	 * Locking: contact read, message write, messageStatus write.
+	 * Locking: contact read, message write, messageFlag write,
+	 * messageStatus write.
 	 */
 	void removeMessage(T txn, MessageId m) throws DbException;
 
@@ -458,8 +474,8 @@ interface Database<T> {
 	 * Unsubscribes from the given group. Any messages belonging to the group
 	 * are deleted from the database.
 	 * <p>
-	 * Locking: contact read, message write, messageStatus write,
-	 * subscription write.
+	 * Locking: contact read, message write, messageFlag write,
+	 * messageStatus write, subscription write.
 	 */
 	void removeSubscription(T txn, GroupId g) throws DbException;
 
@@ -497,6 +513,14 @@ interface Database<T> {
 	 */
 	Rating setRating(T txn, AuthorId a, Rating r) throws DbException;
 
+	/**
+	 * Marks the given message read or unread and returns true if it was
+	 * previously read.
+	 * <p>
+	 * Locking: message read, messageFlag write.
+	 */
+	boolean setRead(T txn, MessageId m, boolean read) throws DbException;
+
 	/**
 	 * Sets the sendability score of the given message.
 	 * <p>
@@ -504,6 +528,14 @@ interface Database<T> {
 	 */
 	void setSendability(T txn, MessageId m, int sendability) throws DbException;
 
+	/**
+	 * Marks the given message starred or unstarred and returns true if it was
+	 * previously starred.
+	 * <p>
+	 * Locking: message read, messageFlag write.
+	 */
+	boolean setStarred(T txn, MessageId m, boolean starred) throws DbException;
+
 	/**
 	 * Sets the status of the given message with respect to the given contact.
 	 * <p>
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 90523a273abea6575755d44d343332bf05609464..36c88cce8d1a7110e9a1641528bc76a7df605d82 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -81,6 +81,8 @@ DatabaseCleaner.Callback {
 		new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock messageLock =
 		new ReentrantReadWriteLock(true);
+	private final ReentrantReadWriteLock messageFlagLock =
+		new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock messageStatusLock =
 		new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock ratingLock =
@@ -1137,28 +1139,33 @@ DatabaseCleaner.Callback {
 		try {
 			messageLock.writeLock().lock();
 			try {
-				messageStatusLock.writeLock().lock();
+				messageFlagLock.writeLock().lock();
 				try {
-					subscriptionLock.writeLock().lock();
+					messageStatusLock.writeLock().lock();
 					try {
-						transportLock.writeLock().lock();
+						subscriptionLock.writeLock().lock();
 						try {
-							T txn = db.startTransaction();
+							transportLock.writeLock().lock();
 							try {
-								db.removeContact(txn, c);
-								db.commitTransaction(txn);
-							} catch(DbException e) {
-								db.abortTransaction(txn);
-								throw e;
+								T txn = db.startTransaction();
+								try {
+									db.removeContact(txn, c);
+									db.commitTransaction(txn);
+								} catch(DbException e) {
+									db.abortTransaction(txn);
+									throw e;
+								}
+							} finally {
+								transportLock.writeLock().unlock();
 							}
 						} finally {
-							transportLock.writeLock().unlock();
+							subscriptionLock.writeLock().unlock();
 						}
 					} finally {
-						subscriptionLock.writeLock().unlock();
+						messageStatusLock.writeLock().unlock();
 					}
 				} finally {
-					messageStatusLock.writeLock().unlock();
+					messageFlagLock.writeLock().unlock();
 				}
 			} finally {
 				messageLock.writeLock().unlock();
@@ -1399,26 +1406,31 @@ DatabaseCleaner.Callback {
 		try {
 			messageLock.writeLock().lock();
 			try {
-				messageStatusLock.writeLock().lock();
+				messageFlagLock.writeLock().lock();
 				try {
-					subscriptionLock.writeLock().lock();
+					messageStatusLock.writeLock().lock();
 					try {
-						T txn = db.startTransaction();
+						subscriptionLock.writeLock().lock();
 						try {
-							if(db.containsSubscription(txn, g)) {
-								affected = db.getVisibility(txn, g);
-								db.removeSubscription(txn, g);
+							T txn = db.startTransaction();
+							try {
+								if(db.containsSubscription(txn, g)) {
+									affected = db.getVisibility(txn, g);
+									db.removeSubscription(txn, g);
+								}
+								db.commitTransaction(txn);
+							} catch(DbException e) {
+								db.abortTransaction(txn);
+								throw e;
 							}
-							db.commitTransaction(txn);
-						} catch(DbException e) {
-							db.abortTransaction(txn);
-							throw e;
+						} finally {
+							subscriptionLock.writeLock().unlock();
 						}
 					} finally {
-						subscriptionLock.writeLock().unlock();
+						messageStatusLock.writeLock().unlock();
 					}
 				} finally {
-					messageStatusLock.writeLock().unlock();
+					messageFlagLock.writeLock().unlock();
 				}
 			} finally {
 				messageLock.writeLock().unlock();
@@ -1457,19 +1469,24 @@ DatabaseCleaner.Callback {
 		try {
 			messageLock.writeLock().lock();
 			try {
-				messageStatusLock.writeLock().lock();
+				messageFlagLock.writeLock().lock();
 				try {
-					T txn = db.startTransaction();
+					messageStatusLock.writeLock().lock();
 					try {
-						old = db.getOldMessages(txn, size);
-						for(MessageId m : old) removeMessage(txn, m);
-						db.commitTransaction(txn);
-					} catch(DbException e) {
-						db.abortTransaction(txn);
-						throw e;
+						T txn = db.startTransaction();
+						try {
+							old = db.getOldMessages(txn, size);
+							for(MessageId m : old) removeMessage(txn, m);
+							db.commitTransaction(txn);
+						} catch(DbException e) {
+							db.abortTransaction(txn);
+							throw e;
+						}
+					} finally {
+						messageStatusLock.writeLock().unlock();
 					}
 				} finally {
-					messageStatusLock.writeLock().unlock();
+					messageFlagLock.writeLock().unlock();
 				}
 			} finally {
 				messageLock.writeLock().unlock();
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index 16799e6bc3093e0d77f09331bedc724ab606da4a..f167aa121cc8c27e9e2d8a3899f7e4b925a21884 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -225,6 +225,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
 		+ " ON DELETE CASCADE)";
 
+	private static final String CREATE_FLAGS =
+		"CREATE TABLE flags"
+		+ " (messageId HASH NOT NULL,"
+		+ " read BOOLEAN NOT NULL,"
+		+ " starred BOOLEAN NOT NULL,"
+		+ " PRIMARY KEY (messageId),"
+		+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
+		+ " ON DELETE CASCADE)";
+
 	private static final Logger LOG =
 		Logger.getLogger(JdbcDatabase.class.getName());
 
@@ -233,6 +242,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private final ConnectionWindowFactory connectionWindowFactory;
 	private final GroupFactory groupFactory;
 	private final MessageHeaderFactory messageHeaderFactory;
+
 	private final LinkedList<Connection> connections =
 		new LinkedList<Connection>(); // Locking: self
 
@@ -316,6 +326,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_CONNECTION_WINDOWS));
 			s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_TIMESTAMPS));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_TIMESTAMPS));
+			s.executeUpdate(insertTypeNames(CREATE_FLAGS));
 			s.close();
 		} catch(SQLException e) {
 			tryToClose(s);
@@ -1319,6 +1330,27 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public boolean getRead(Connection txn, MessageId m) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT read FROM flags WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			rs = ps.executeQuery();
+			boolean read = false;
+			if(rs.next()) read = rs.getBoolean(1);
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return read;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Map<ContactId, TransportProperties> getRemoteProperties(
 			Connection txn, TransportId t) throws DbException {
 		PreparedStatement ps = null;
@@ -1513,6 +1545,27 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public boolean getStarred(Connection txn, MessageId m) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT starred FROM flags WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			rs = ps.executeQuery();
+			boolean starred = false;
+			if(rs.next()) starred = rs.getBoolean(1);
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return starred;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Collection<Group> getSubscriptions(Connection txn)
 	throws DbException {
 		PreparedStatement ps = null;
@@ -2068,7 +2121,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs = ps.executeQuery();
 			Rating old;
 			if(rs.next()) {
-				// A rating row exists - update it
+				// A rating row exists - update it if necessary
 				old = Rating.values()[rs.getByte(1)];
 				if(rs.next()) throw new DbStateException();
 				rs.close();
@@ -2083,17 +2136,70 @@ abstract class JdbcDatabase implements Database<Connection> {
 					ps.close();
 				}
 			} else {
-				// No rating row exists - create one
+				// No rating row exists - create one if necessary
 				rs.close();
 				ps.close();
 				old = Rating.UNRATED;
-				sql = "INSERT INTO ratings (authorId, rating) VALUES (?, ?)";
-				ps = txn.prepareStatement(sql);
-				ps.setBytes(1, a.getBytes());
-				ps.setShort(2, (short) r.ordinal());
-				int affected = ps.executeUpdate();
-				if(affected != 1) throw new DbStateException();
+				if(!old.equals(r)) {
+					sql = "INSERT INTO ratings (authorId, rating)"
+						+ " VALUES (?, ?)";
+					ps = txn.prepareStatement(sql);
+					ps.setBytes(1, a.getBytes());
+					ps.setShort(2, (short) r.ordinal());
+					int affected = ps.executeUpdate();
+					if(affected != 1) throw new DbStateException();
+					ps.close();
+				}
+			}
+			return old;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public boolean setRead(Connection txn, MessageId m, boolean read)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT read FROM flags WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			rs = ps.executeQuery();
+			boolean old;
+			if(rs.next()) {
+				// A flag row exists - update it if necessary
+				old = rs.getBoolean(1);
+				if(rs.next()) throw new DbStateException();
+				rs.close();
 				ps.close();
+				if(old != read) {
+					sql = "UPDATE flags SET read = ? WHERE messageId = ?";
+					ps = txn.prepareStatement(sql);
+					ps.setBoolean(1, read);
+					ps.setBytes(2, m.getBytes());
+					int affected = ps.executeUpdate();
+					if(affected != 1) throw new DbStateException();
+					ps.close();
+				}
+			} else {
+				// No flag row exists - create one if necessary
+				ps.close();
+				rs.close();
+				old = false;
+				if(old != read) {
+					sql = "INSERT INTO flags (messageId, read, starred)"
+						+ " VALUES (?, ?, ?)";
+					ps = txn.prepareStatement(sql);
+					ps.setBytes(1, m.getBytes());
+					ps.setBoolean(2, read);
+					ps.setBoolean(3, false);
+					int affected = ps.executeUpdate();
+					if(affected != 1) throw new DbStateException();
+					ps.close();
+				}
 			}
 			return old;
 		} catch(SQLException e) {
@@ -2121,6 +2227,56 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public boolean setStarred(Connection txn, MessageId m, boolean starred)
+	throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT starred FROM flags WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			rs = ps.executeQuery();
+			boolean old;
+			if(rs.next()) {
+				// A flag row exists - update it if necessary
+				old = rs.getBoolean(1);
+				if(rs.next()) throw new DbStateException();
+				rs.close();
+				ps.close();
+				if(old != starred) {
+					sql = "UPDATE flags SET starred = ? WHERE messageId = ?";
+					ps = txn.prepareStatement(sql);
+					ps.setBoolean(1, starred);
+					ps.setBytes(2, m.getBytes());
+					int affected = ps.executeUpdate();
+					if(affected != 1) throw new DbStateException();
+					ps.close();
+				}
+			} else {
+				// No flag row exists - create one if necessary
+				ps.close();
+				rs.close();
+				old = false;
+				if(old != starred) {
+					sql = "INSERT INTO flags (messageId, read, starred)"
+						+ " VALUES (?, ?, ?)";
+					ps = txn.prepareStatement(sql);
+					ps.setBytes(1, m.getBytes());
+					ps.setBoolean(2, false);
+					ps.setBoolean(3, starred);
+					int affected = ps.executeUpdate();
+					if(affected != 1) throw new DbStateException();
+					ps.close();
+				}
+			}
+			return old;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void setStatus(Connection txn, ContactId c, MessageId m, Status s)
 	throws DbException {
 		PreparedStatement ps = null;
@@ -2133,7 +2289,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setInt(2, c.getInt());
 			rs = ps.executeQuery();
 			if(rs.next()) {
-				// A status row exists - update it
+				// A status row exists - update it if necessary
 				Status old = Status.values()[rs.getByte(1)];
 				if(rs.next()) throw new DbStateException();
 				rs.close();
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index edc59a800e2d42f7d4621c2691107e03ec2829f2..b2f5b16c8705a712cc96b911ebc6198fa388c8e0 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -1705,6 +1705,58 @@ public class H2DatabaseTest extends TestCase {
 		db.close();
 	}
 
+	@Test
+	public void testReadFlag() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Subscribe to a group and store a message
+		db.addSubscription(txn, group);
+		db.addGroupMessage(txn, message);
+
+		// The message should be unread by default
+		assertFalse(db.getRead(txn, messageId));
+		// Marking the message read should return the old value
+		assertFalse(db.setRead(txn, messageId, true));
+		assertTrue(db.setRead(txn, messageId, true));
+		// The message should be read
+		assertTrue(db.getRead(txn, messageId));
+		// Marking the message unread should return the old value
+		assertTrue(db.setRead(txn, messageId, false));
+		assertFalse(db.setRead(txn, messageId, false));
+		// Unsubscribe from the group
+		db.removeSubscription(txn, groupId);
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
+	@Test
+	public void testStarredFlag() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Subscribe to a group and store a message
+		db.addSubscription(txn, group);
+		db.addGroupMessage(txn, message);
+
+		// The message should be unstarred by default
+		assertFalse(db.getStarred(txn, messageId));
+		// Starring the message should return the old value
+		assertFalse(db.setStarred(txn, messageId, true));
+		assertTrue(db.setStarred(txn, messageId, true));
+		// The message should be starred
+		assertTrue(db.getStarred(txn, messageId));
+		// Unstarring the message should return the old value
+		assertTrue(db.setStarred(txn, messageId, false));
+		assertFalse(db.setStarred(txn, messageId, false));
+		// Unsubscribe from the group
+		db.removeSubscription(txn, groupId);
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	private void assertHeadersAreEqual(MessageHeader h1, MessageHeader h2) {
 		assertEquals(h1.getId(), h2.getId());
 		if(h1.getParent() == null) assertNull(h2.getParent());