diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
index e955ea3a477061a32928660fceb4e4023527193f..565f9f1f8d48a423320bba341d880c321313a532 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
@@ -8,7 +8,7 @@ abstract class ConversationIntroductionItem extends ConversationItem {
 	private boolean answered;
 
 	public ConversationIntroductionItem(IntroductionRequest ir) {
-		super(ir.getMessageId(), ir.getTime());
+		super(ir.getMessageId(), ir.getTimestamp());
 
 		this.ir = ir;
 		this.answered = ir.wasAnswered();
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
index 76eb803a35f96f02c9df891af873d6605f08f2e4..2fc96b6ab7e54857c3bb53f212b45c50c18fda67 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
@@ -69,7 +69,7 @@ public abstract class ConversationItem {
 						ir.getName());
 			}
 			return new ConversationNoticeOutItem(ir.getMessageId(), text,
-					ir.getTime(), ir.isSent(), ir.isSeen());
+					ir.getTimestamp(), ir.isSent(), ir.isSeen());
 		} else {
 			String text;
 			if (ir.wasAccepted()) {
@@ -88,7 +88,7 @@ public abstract class ConversationItem {
 				}
 			}
 			return new ConversationNoticeInItem(ir.getMessageId(), text,
-					ir.getTime(), ir.isRead());
+					ir.getTimestamp(), ir.isRead());
 		}
 	}
 
@@ -98,9 +98,9 @@ public abstract class ConversationItem {
 	public static ConversationItem from(IntroductionMessage im) {
 		if (im.isLocal())
 			return new ConversationNoticeOutItem(im.getMessageId(), "",
-					im.getTime(), false, false);
-		return new ConversationNoticeInItem(im.getMessageId(), "", im.getTime(),
-				im.isRead());
+					im.getTimestamp(), false, false);
+		return new ConversationNoticeInItem(im.getMessageId(), "",
+				im.getTimestamp(), im.isRead());
 	}
 
 	protected interface OutgoingItem {
diff --git a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
index eb4ade9abee989407225653dd7b97b6ad6b55c12..f685e0d0670c8e169aff0a2e89de7b9b37d6db97 100644
--- a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
@@ -171,7 +171,6 @@ implements EventListener, OnItemClickListener {
 			public void run() {
 				try {
 					forumManager.addForum(f);
-					forumSharingManager.setSharedWith(f.getId(), shared);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
index cccecbc7ae4c97a293edb50ab1d908f18870d8d8..ccb0b365d907f65b419929850c9b1d7ea369279e 100644
--- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
@@ -91,7 +91,6 @@ public class ShareForumActivity extends BriarActivity implements
 				onBackPressed();
 				return true;
 			case R.id.action_share_forum:
-				storeVisibility();
 				return true;
 			default:
 				return super.onOptionsItemSelected(item);
@@ -140,26 +139,6 @@ public class ShareForumActivity extends BriarActivity implements
 		});
 	}
 
-	private void storeVisibility() {
-		runOnDbThread(new Runnable() {
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					Collection<ContactId> selected =
-							adapter.getSelectedContactIds();
-					forumSharingManager.setSharedWith(groupId, selected);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Update took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-				finishOnUiThread();
-			}
-		});
-	}
-
 	@Override
 	public void onItemClick(View view, ContactListItem item) {
 		((SelectableContactListItem) item).toggleSelected();
diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d8e7b5f7b61a1a88076277e48fb3b52cc34de13
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/ForumInvitationReceivedEvent.java
@@ -0,0 +1,24 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.introduction.IntroductionRequest;
+
+public class ForumInvitationReceivedEvent extends Event {
+
+	private final Forum forum;
+	private final ContactId contactId;
+
+	public ForumInvitationReceivedEvent(Forum forum, ContactId contactId) {
+		this.forum = forum;
+		this.contactId = contactId;
+	}
+
+	public Forum getForum() {
+		return forum;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e799240386966ff59c73037c97e6cfaaaeb2e63
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/ForumInvitationResponseReceivedEvent.java
@@ -0,0 +1,24 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+
+public class ForumInvitationResponseReceivedEvent extends Event {
+
+	private final String forumName;
+	private final ContactId contactId;
+
+	public ForumInvitationResponseReceivedEvent(String forumName,
+			ContactId contactId) {
+
+		this.forumName = forumName;
+		this.contactId = contactId;
+	}
+
+	public String getForumName() {
+		return forumName;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/forum/ForumConstants.java b/briar-api/src/org/briarproject/api/forum/ForumConstants.java
index 035a06cd1ef800f91b1789fa3ae3f51da0c5f457..dcdee0132b9d1d807ca402c12e5972e3cf85ce09 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumConstants.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumConstants.java
@@ -15,4 +15,38 @@ public interface ForumConstants {
 
 	/** The maximum length of a forum post's body in bytes. */
 	int MAX_FORUM_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
+
+	/* Forum Sharing Constants */
+	String CONTACT_ID = "contactId";
+	String GROUP_ID = "groupId";
+	String TO_BE_SHARED_BY_US = "toBeSharedByUs";
+	String SHARED_BY_US = "sharedByUs";
+	String SHARED_WITH_US = "sharedWithUs";
+	String TYPE = "type";
+	String SESSION_ID = "sessionId";
+	String STORAGE_ID = "storageId";
+	String STATE = "state";
+	String LOCAL = "local";
+	String TIME = "time";
+	String READ = "read";
+	String IS_SHARER = "isSharer";
+	String FORUM_ID = "forumId";
+	String FORUM_NAME = "forumName";
+	String FORUM_SALT = "forumSalt";
+	String INVITATION_MSG = "invitationMsg";
+	int SHARE_MSG_TYPE_INVITATION = 1;
+	int SHARE_MSG_TYPE_ACCEPT = 2;
+	int SHARE_MSG_TYPE_DECLINE = 3;
+	int SHARE_MSG_TYPE_LEAVE = 4;
+	int SHARE_MSG_TYPE_ABORT = 5;
+	String TASK = "task";
+	int TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US = 0;
+	int TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US = 1;
+	int TASK_ADD_SHARED_FORUM = 2;
+	int TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US = 3;
+	int TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US = 4;
+	int TASK_SHARE_FORUM = 5;
+	int TASK_UNSHARE_FORUM_SHARED_BY_US = 6;
+	int TASK_UNSHARE_FORUM_SHARED_WITH_US = 7;
+
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java b/briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..b153c27f11cb996d5b48f3ad3ebb4aa47e17475d
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/ForumInvitationMessage.java
@@ -0,0 +1,48 @@
+package org.briarproject.api.forum;
+
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.messaging.BaseMessage;
+import org.briarproject.api.sync.MessageId;
+
+public class ForumInvitationMessage extends BaseMessage {
+
+	private final SessionId sessionId;
+	private final ContactId contactId;
+	private final String forumName, message;
+	private final boolean available;
+
+	public ForumInvitationMessage(MessageId id, SessionId sessionId,
+			ContactId contactId, String forumName, String message,
+			boolean available, long time, boolean local, boolean sent,
+			boolean seen, boolean read) {
+
+		super(id, time, local, read, sent, seen);
+		this.sessionId = sessionId;
+		this.contactId = contactId;
+		this.forumName = forumName;
+		this.message = message;
+		this.available = available;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public String getForumName() {
+		return forumName;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public boolean isAvailable() {
+		return available;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
index e3e3b1191cca67bad368ab612018add384775ff5..9f4cc87b6620aebb593a32f3f2e9e3655207aa9a 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
@@ -1,5 +1,6 @@
 package org.briarproject.api.forum;
 
+import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
@@ -13,20 +14,35 @@ public interface ForumSharingManager {
 	/** Returns the unique ID of the forum sharing client. */
 	ClientId getClientId();
 
+	/**
+	 * Sends an invitation to share the given forum with the given contact
+	 * and sends an optional message along with it.
+	 */
+	void sendForumInvitation(GroupId groupId, ContactId contactId,
+			String message)	throws DbException;
+
+	/**
+	 * Responds to a pending forum invitation
+	 */
+	void respondToInvitation(Forum f, boolean accept) throws DbException;
+
+	/**
+	 * Returns all forum sharing messages sent by the Contact
+	 * identified by contactId.
+	 */
+	Collection<ForumInvitationMessage> getForumInvitationMessages(
+			ContactId contactId) throws DbException;
+
 	/** Returns all forums to which the user could subscribe. */
 	Collection<Forum> getAvailableForums() throws DbException;
 
-	/** Returns all contacts who are sharing the given forum with the user. */
+	/** Returns all contacts who are sharing the given forum with us. */
 	Collection<Contact> getSharedBy(GroupId g) throws DbException;
 
 	/** Returns the IDs of all contacts with whom the given forum is shared. */
 	Collection<ContactId> getSharedWith(GroupId g) throws DbException;
 
-	/**
-	 * Shares a forum with the given contacts and unshares it with any other
-	 * contacts.
-	 */
-	void setSharedWith(GroupId g, Collection<ContactId> shared)
-			throws DbException;
+	/** Returns true if the forum not already shared and no invitation is open */
+	boolean canBeShared(GroupId g, Contact c) throws DbException;
 
 }
diff --git a/briar-api/src/org/briarproject/api/forum/InviteeAction.java b/briar-api/src/org/briarproject/api/forum/InviteeAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..212f0861ce661de2e69b18adeb8a52ba9aa14af5
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/InviteeAction.java
@@ -0,0 +1,34 @@
+package org.briarproject.api.forum;
+
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE;
+
+public enum InviteeAction {
+
+	LOCAL_ACCEPT,
+	LOCAL_DECLINE,
+	LOCAL_LEAVE,
+	LOCAL_ABORT,
+	REMOTE_INVITATION,
+	REMOTE_LEAVE,
+	REMOTE_ABORT;
+
+	public static InviteeAction getLocal(long type) {
+		if (type == SHARE_MSG_TYPE_ACCEPT) return LOCAL_ACCEPT;
+		if (type == SHARE_MSG_TYPE_DECLINE) return LOCAL_DECLINE;
+		if (type == SHARE_MSG_TYPE_LEAVE) return LOCAL_LEAVE;
+		if (type == SHARE_MSG_TYPE_ABORT) return LOCAL_ABORT;
+		return null;
+	}
+
+	public static InviteeAction getRemote(long type) {
+		if (type == SHARE_MSG_TYPE_INVITATION) return REMOTE_INVITATION;
+		if (type == SHARE_MSG_TYPE_LEAVE) return REMOTE_LEAVE;
+		if (type == SHARE_MSG_TYPE_ABORT) return REMOTE_ABORT;
+		return null;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java b/briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java
new file mode 100644
index 0000000000000000000000000000000000000000..35d6a880af0b9bbb41c3ffd2ede3122d9b7fb297
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/InviteeProtocolState.java
@@ -0,0 +1,62 @@
+package org.briarproject.api.forum;
+
+import static org.briarproject.api.forum.InviteeAction.LOCAL_ACCEPT;
+import static org.briarproject.api.forum.InviteeAction.LOCAL_DECLINE;
+import static org.briarproject.api.forum.InviteeAction.LOCAL_LEAVE;
+import static org.briarproject.api.forum.InviteeAction.REMOTE_INVITATION;
+import static org.briarproject.api.forum.InviteeAction.REMOTE_LEAVE;
+
+public enum InviteeProtocolState {
+
+	ERROR(0),
+	AWAIT_INVITATION(1) {
+		@Override
+		public InviteeProtocolState next(InviteeAction a) {
+			if (a == REMOTE_INVITATION) return AWAIT_LOCAL_RESPONSE;
+			return ERROR;
+		}
+	},
+	AWAIT_LOCAL_RESPONSE(2) {
+		@Override
+		public InviteeProtocolState next(InviteeAction a) {
+			if (a == LOCAL_ACCEPT || a == LOCAL_DECLINE) return FINISHED;
+			if (a == REMOTE_LEAVE) return LEFT;
+			return ERROR;
+		}
+	},
+	FINISHED(3) {
+		@Override
+		public InviteeProtocolState next(InviteeAction a) {
+			if (a == LOCAL_LEAVE || a == REMOTE_LEAVE) return LEFT;
+			return FINISHED;
+		}
+	},
+	LEFT(4) {
+		@Override
+		public InviteeProtocolState next(InviteeAction a) {
+			if (a == LOCAL_LEAVE) return ERROR;
+			return LEFT;
+		}
+	};
+
+	private final int value;
+
+	InviteeProtocolState(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public static InviteeProtocolState fromValue(int value) {
+		for (InviteeProtocolState s : values()) {
+			if (s.value == value) return s;
+		}
+		throw new IllegalArgumentException();
+	}
+
+	public InviteeProtocolState next(InviteeAction a) {
+		return this;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/forum/SharerAction.java b/briar-api/src/org/briarproject/api/forum/SharerAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..39796f2c828c10f01bcd6ae3a05f7d258792e11e
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/SharerAction.java
@@ -0,0 +1,34 @@
+package org.briarproject.api.forum;
+
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE;
+
+public enum SharerAction {
+
+	LOCAL_INVITATION,
+	LOCAL_LEAVE,
+	LOCAL_ABORT,
+	REMOTE_ACCEPT,
+	REMOTE_DECLINE,
+	REMOTE_LEAVE,
+	REMOTE_ABORT;
+
+	public static SharerAction getLocal(long type) {
+		if (type == SHARE_MSG_TYPE_INVITATION) return LOCAL_INVITATION;
+		if (type == SHARE_MSG_TYPE_LEAVE) return LOCAL_LEAVE;
+		if (type == SHARE_MSG_TYPE_ABORT) return LOCAL_ABORT;
+		return null;
+	}
+
+	public static SharerAction getRemote(long type) {
+		if (type == SHARE_MSG_TYPE_ACCEPT) return REMOTE_ACCEPT;
+		if (type == SHARE_MSG_TYPE_DECLINE) return REMOTE_DECLINE;
+		if (type == SHARE_MSG_TYPE_LEAVE) return REMOTE_LEAVE;
+		if (type == SHARE_MSG_TYPE_ABORT) return REMOTE_ABORT;
+		return null;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/forum/SharerProtocolState.java b/briar-api/src/org/briarproject/api/forum/SharerProtocolState.java
new file mode 100644
index 0000000000000000000000000000000000000000..b948a9483d980d1cd9a0eeed91d81a6edcdf5511
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/SharerProtocolState.java
@@ -0,0 +1,62 @@
+package org.briarproject.api.forum;
+
+import static org.briarproject.api.forum.SharerAction.LOCAL_INVITATION;
+import static org.briarproject.api.forum.SharerAction.LOCAL_LEAVE;
+import static org.briarproject.api.forum.SharerAction.REMOTE_ACCEPT;
+import static org.briarproject.api.forum.SharerAction.REMOTE_DECLINE;
+import static org.briarproject.api.forum.SharerAction.REMOTE_LEAVE;
+
+public enum SharerProtocolState {
+
+	ERROR(0),
+	PREPARE_INVITATION(1) {
+		@Override
+		public SharerProtocolState next(SharerAction a) {
+			if (a == LOCAL_INVITATION) return AWAIT_RESPONSE;
+			return ERROR;
+		}
+	},
+	AWAIT_RESPONSE(2) {
+		@Override
+		public SharerProtocolState next(SharerAction a) {
+			if (a == REMOTE_ACCEPT || a == REMOTE_DECLINE) return FINISHED;
+			if (a == LOCAL_LEAVE) return LEFT;
+			return ERROR;
+		}
+	},
+	FINISHED(3) {
+		@Override
+		public SharerProtocolState next(SharerAction a) {
+			if (a == LOCAL_LEAVE || a == REMOTE_LEAVE) return LEFT;
+			return FINISHED;
+		}
+	},
+	LEFT(4) {
+		@Override
+		public SharerProtocolState next(SharerAction a) {
+			if (a == LOCAL_LEAVE) return ERROR;
+			return LEFT;
+		}
+	};
+
+	private final int value;
+
+	SharerProtocolState(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public static SharerProtocolState fromValue(int value) {
+		for (SharerProtocolState s : values()) {
+			if (s.value == value) return s;
+		}
+		throw new IllegalArgumentException();
+	}
+
+	public SharerProtocolState next(SharerAction a) {
+		return this;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
index 8c9de76dfaf8eea628c8bd67946429acc1858918..d860726a7565fe5a613e574e55c19bf1524b908d 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
@@ -1,61 +1,36 @@
 package org.briarproject.api.introduction;
 
 import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.messaging.BaseMessage;
 import org.briarproject.api.sync.MessageId;
 
 import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
 import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
 
-abstract public class IntroductionMessage {
+ public class IntroductionMessage extends BaseMessage {
 
 	private final SessionId sessionId;
 	private final MessageId messageId;
 	private final int role;
-	private final long time;
-	private final boolean local, sent, seen, read;
 
 	public IntroductionMessage(SessionId sessionId, MessageId messageId,
 			int role, long time, boolean local, boolean sent, boolean seen,
 			boolean read) {
 
+		super(messageId, time, local, read, sent, seen);
 		this.sessionId = sessionId;
 		this.messageId = messageId;
 		this.role = role;
-		this.time = time;
-		this.local = local;
-		this.sent = sent;
-		this.seen = seen;
-		this.read = read;
 	}
 
 	public SessionId getSessionId() {
 		return sessionId;
 	}
 
-	public long getTime() {
-		return time;
-	}
-
 	public MessageId getMessageId() {
 		return messageId;
 	}
 
-	public boolean isLocal() {
-		return local;
-	}
-
-	public boolean isSent() {
-		return sent;
-	}
-
-	public boolean isSeen() {
-		return seen;
-	}
-
-	public boolean isRead() {
-		return read;
-	}
-
 	public boolean isIntroducer() {
 		return role == ROLE_INTRODUCER;
 	}
diff --git a/briar-api/src/org/briarproject/api/messaging/BaseMessage.java b/briar-api/src/org/briarproject/api/messaging/BaseMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..c83350a09e4c28feffb874bae9038077669c84b9
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/messaging/BaseMessage.java
@@ -0,0 +1,45 @@
+package org.briarproject.api.messaging;
+
+import org.briarproject.api.sync.MessageId;
+
+public abstract class BaseMessage {
+
+	private final MessageId id;
+	private final long timestamp;
+	private final boolean local, read, sent, seen;
+
+	public BaseMessage(MessageId id, long timestamp, boolean local,
+			boolean read, boolean sent, boolean seen) {
+
+		this.id = id;
+		this.timestamp = timestamp;
+		this.local = local;
+		this.read = read;
+		this.sent = sent;
+		this.seen = seen;
+	}
+
+	public MessageId getId() {
+		return id;
+	}
+
+	public long getTimestamp() {
+		return timestamp;
+	}
+
+	public boolean isLocal() {
+		return local;
+	}
+
+	public boolean isRead() {
+		return read;
+	}
+
+	public boolean isSent() {
+		return sent;
+	}
+
+	public boolean isSeen() {
+		return seen;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java
index 9db8854a1d819a04df07b8446fd30adaa8ea4ff4..f1c8eb51fe6d0246e99e16b0c7435c501e7bb7e4 100644
--- a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java
+++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java
@@ -2,50 +2,19 @@ package org.briarproject.api.messaging;
 
 import org.briarproject.api.sync.MessageId;
 
-public class PrivateMessageHeader {
+public class PrivateMessageHeader extends BaseMessage {
 
-	private final MessageId id;
-	private final long timestamp;
 	private final String contentType;
-	private final boolean local, read, sent, seen;
 
 	public PrivateMessageHeader(MessageId id, long timestamp,
 			String contentType, boolean local, boolean read, boolean sent,
 			boolean seen) {
-		this.id = id;
-		this.timestamp = timestamp;
-		this.contentType = contentType;
-		this.local = local;
-		this.read = read;
-		this.sent = sent;
-		this.seen = seen;
-	}
 
-	public MessageId getId() {
-		return id;
+		super(id, timestamp, local, read, sent, seen);
+		this.contentType = contentType;
 	}
 
 	public String getContentType() {
 		return contentType;
 	}
-
-	public long getTimestamp() {
-		return timestamp;
-	}
-
-	public boolean isLocal() {
-		return local;
-	}
-
-	public boolean isRead() {
-		return read;
-	}
-
-	public boolean isSent() {
-		return sent;
-	}
-
-	public boolean isSeen() {
-		return seen;
-	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumListValidator.java b/briar-core/src/org/briarproject/forum/ForumListValidator.java
deleted file mode 100644
index 9e6947a377f4d12b4d02bff1a6b380a1831798e6..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/forum/ForumListValidator.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.briarproject.forum;
-
-import org.briarproject.api.FormatException;
-import org.briarproject.api.clients.ClientHelper;
-import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfList;
-import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.Message;
-import org.briarproject.api.system.Clock;
-import org.briarproject.clients.BdfMessageValidator;
-
-import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
-import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
-
-class ForumListValidator extends BdfMessageValidator {
-
-	ForumListValidator(ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-		super(clientHelper, metadataEncoder, clock);
-	}
-
-	@Override
-	protected BdfDictionary validateMessage(Message m, Group g,
-			BdfList body) throws FormatException {
-		// Version, forum list
-		checkSize(body, 2);
-		// Version
-		long version = body.getLong(0);
-		if (version < 0) throw new FormatException();
-		// Forum list
-		BdfList forumList = body.getList(1);
-		for (int i = 0; i < forumList.size(); i++) {
-			BdfList forum = forumList.getList(i);
-			// Name, salt
-			checkSize(forum, 2);
-			String name = forum.getString(0);
-			checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
-			byte[] salt = forum.getRaw(1);
-			checkLength(salt, FORUM_SALT_LENGTH);
-		}
-		// Return the metadata
-		BdfDictionary meta = new BdfDictionary();
-		meta.put("version", version);
-		meta.put("local", false);
-		return meta;
-	}
-}
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index ac0ff20616b31e0c7de10f3eb345fb7350e6046d..e6d7b55ac4197043c14d50185053926ea1463253 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -1,6 +1,7 @@
 package org.briarproject.forum;
 
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.MetadataEncoder;
@@ -26,11 +27,11 @@ import dagger.Provides;
 public class ForumModule {
 
 	public static class EagerSingletons {
-		@Inject
-		ForumListValidator forumListValidator;
 		@Inject
 		ForumPostValidator forumPostValidator;
 		@Inject
+		ForumSharingValidator forumSharingValidator;
+		@Inject
 		ForumSharingManager forumSharingManager;
 	}
 
@@ -63,13 +64,15 @@ public class ForumModule {
 
 	@Provides
 	@Singleton
-	ForumListValidator provideForumListValidator(
-			ValidationManager validationManager, ClientHelper clientHelper,
+	ForumSharingValidator provideSharingValidator(
+			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
 			MetadataEncoder metadataEncoder, Clock clock) {
-		ForumListValidator validator = new ForumListValidator(clientHelper,
+
+		ForumSharingValidator validator = new ForumSharingValidator(clientHelper,
 				metadataEncoder, clock);
-		validationManager.registerMessageValidator(
+		messageQueueManager.registerMessageValidator(
 				ForumSharingManagerImpl.CLIENT_ID, validator);
+
 		return validator;
 	}
 
@@ -78,15 +81,17 @@ public class ForumModule {
 	ForumSharingManager provideForumSharingManager(
 			LifecycleManager lifecycleManager,
 			ContactManager contactManager,
-			ValidationManager validationManager,
+			MessageQueueManager messageQueueManager,
 			ForumManager forumManager,
 			ForumSharingManagerImpl forumSharingManager) {
+
 		lifecycleManager.registerClient(forumSharingManager);
 		contactManager.registerAddContactHook(forumSharingManager);
 		contactManager.registerRemoveContactHook(forumSharingManager);
-		validationManager.registerIncomingMessageHook(
+		messageQueueManager.registerIncomingMessageHook(
 				ForumSharingManagerImpl.CLIENT_ID, forumSharingManager);
 		forumManager.registerRemoveForumHook(forumSharingManager);
+
 		return forumSharingManager;
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
index 62501e8e50ccd007c3d5bd9809fb28e882491999..c970571a9c02523cea3db2b5844f346de72502c6 100644
--- a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
@@ -1,74 +1,140 @@
 package org.briarproject.forum;
 
+import org.briarproject.api.Bytes;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageQueueManager;
 import org.briarproject.api.clients.PrivateGroupFactory;
+import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfEntry;
 import org.briarproject.api.data.BdfList;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.NoSuchMessageException;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.event.Event;
 import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumInvitationMessage;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.forum.InviteeAction;
+import org.briarproject.api.forum.InviteeProtocolState;
+import org.briarproject.api.forum.SharerAction;
+import org.briarproject.api.forum.SharerProtocolState;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
-import org.briarproject.api.sync.ValidationManager.IncomingMessageHook;
+import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.api.system.Clock;
+import org.briarproject.clients.BdfIncomingMessageHook;
 import org.briarproject.util.StringUtils;
 
 import java.io.IOException;
+import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
+import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.clients.ProtocolEngine.StateUpdate;
+import static org.briarproject.api.forum.ForumConstants.CONTACT_ID;
+import static org.briarproject.api.forum.ForumConstants.FORUM_ID;
+import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
+import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
+import static org.briarproject.api.forum.ForumConstants.INVITATION_MSG;
+import static org.briarproject.api.forum.ForumConstants.IS_SHARER;
+import static org.briarproject.api.forum.ForumConstants.LOCAL;
+import static org.briarproject.api.forum.ForumConstants.READ;
+import static org.briarproject.api.forum.ForumConstants.SESSION_ID;
+import static org.briarproject.api.forum.ForumConstants.SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.api.forum.ForumConstants.STATE;
+import static org.briarproject.api.forum.ForumConstants.STORAGE_ID;
+import static org.briarproject.api.forum.ForumConstants.TASK;
+import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_ADD_SHARED_FORUM;
+import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_SHARE_FORUM;
+import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.TIME;
+import static org.briarproject.api.forum.ForumConstants.TO_BE_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TYPE;
 import static org.briarproject.api.forum.ForumManager.RemoveForumHook;
+import static org.briarproject.api.forum.InviteeProtocolState.AWAIT_INVITATION;
+import static org.briarproject.api.forum.InviteeProtocolState.AWAIT_LOCAL_RESPONSE;
+import static org.briarproject.api.forum.SharerProtocolState.PREPARE_INVITATION;
 
-class ForumSharingManagerImpl implements ForumSharingManager, Client,
-		AddContactHook, RemoveContactHook, IncomingMessageHook,
-		RemoveForumHook {
+class ForumSharingManagerImpl extends BdfIncomingMessageHook
+		implements ForumSharingManager, Client, RemoveForumHook,
+		AddContactHook, RemoveContactHook {
 
 	static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
 			"cd11a5d04dccd9e2931d6fc3df456313"
 					+ "63bb3e9d9d0e9405fccdb051f41f5449"));
 
+	private static final Logger LOG =
+			Logger.getLogger(ForumSharingManagerImpl.class.getName());
+
 	private final DatabaseComponent db;
 	private final ForumManager forumManager;
-	private final ClientHelper clientHelper;
+	private final MessageQueueManager messageQueueManager;
+	private final MetadataEncoder metadataEncoder;
+	private final SecureRandom random;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final Clock clock;
+	private final Group localGroup;
 
 	@Inject
 	ForumSharingManagerImpl(DatabaseComponent db, ForumManager forumManager,
-			ClientHelper clientHelper, PrivateGroupFactory privateGroupFactory,
+			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
+			MetadataParser metadataParser, MetadataEncoder metadataEncoder,
+			SecureRandom random, PrivateGroupFactory privateGroupFactory,
 			Clock clock) {
 
+		super(clientHelper, metadataParser);
 		this.db = db;
 		this.forumManager = forumManager;
-		this.clientHelper = clientHelper;
+		this.messageQueueManager = messageQueueManager;
+		this.metadataEncoder = metadataEncoder;
+		this.random = random;
 		this.privateGroupFactory = privateGroupFactory;
 		this.clock = clock;
+		localGroup = privateGroupFactory.createLocalGroup(getClientId());
 	}
 
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
+		db.addGroup(txn, localGroup);
 		// Ensure we've set things up for any pre-existing contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
@@ -85,7 +151,10 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
 			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
 			// Attach the contact ID to the group
 			BdfDictionary meta = new BdfDictionary();
-			meta.put("contactId", c.getId().getInt());
+			meta.put(CONTACT_ID, c.getId().getInt());
+			meta.put(TO_BE_SHARED_BY_US, new BdfList());
+			meta.put(SHARED_BY_US, new BdfList());
+			meta.put(SHARED_WITH_US, new BdfList());
 			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
 		} catch (FormatException e) {
 			throw new DbException(e);
@@ -94,30 +163,228 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
+		// clean up session states with that contact from localGroup
+		Long id = (long) c.getId().getInt();
+		try {
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId());
+			for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
+				BdfDictionary d = entry.getValue();
+				if (id.equals(d.getLong(CONTACT_ID))) {
+					deleteMessage(txn, entry.getKey());
+				}
+			}
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+
+		// remove the contact group (all messages will be removed with it)
 		db.removeGroup(txn, getContactGroup(c));
 	}
 
 	@Override
-	public void incomingMessage(Transaction txn, Message m, Metadata meta)
+	protected void incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary msg) throws DbException, FormatException {
+
+		SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
+		long type = msg.getLong(TYPE);
+		if (type == SHARE_MSG_TYPE_INVITATION) {
+			// we are an invitee who just received a new invitation
+			boolean stateExists = true;
+			try {
+				// check if we have a session with that ID already
+				getSessionState(txn, sessionId, false);
+			} catch (FormatException e) {
+				// this is what we would expect under normal circumstances
+				stateExists = false;
+			}
+			try {
+				// check if we already have a state with that sessionId
+				if (stateExists) throw new FormatException();
+
+				// check if forum can be shared
+				Forum f = forumManager.createForum(msg.getString(FORUM_NAME),
+						msg.getRaw(FORUM_SALT));
+				ContactId contactId = getContactId(txn, m.getGroupId());
+				Contact contact = db.getContact(txn, contactId);
+				if (!canBeShared(txn, f.getId(), contact))
+					throw new FormatException();
+
+				// initialize state and process invitation
+				BdfDictionary state =
+						initializeInviteeState(txn, contactId, msg);
+				InviteeEngine engine = new InviteeEngine();
+				processStateUpdate(txn, m.getId(),
+						engine.onMessageReceived(state, msg));
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				deleteMessage(txn, m.getId());
+			}
+		} else if (type == SHARE_MSG_TYPE_ACCEPT ||
+				type == SHARE_MSG_TYPE_DECLINE) {
+			// we are a sharer who just received a response
+			BdfDictionary state = getSessionState(txn, sessionId, true);
+			SharerEngine engine = new SharerEngine();
+			processStateUpdate(txn, m.getId(),
+					engine.onMessageReceived(state, msg));
+		} else if (type == SHARE_MSG_TYPE_LEAVE ||
+				type == SHARE_MSG_TYPE_ABORT) {
+			// we don't know who we are, so figure it out
+			BdfDictionary state = getSessionState(txn, sessionId, true);
+			if (state.getBoolean(IS_SHARER)) {
+				// we are a sharer and the invitee wants to leave or abort
+				SharerEngine engine = new SharerEngine();
+				processStateUpdate(txn, m.getId(),
+						engine.onMessageReceived(state, msg));
+			} else {
+				// we are an invitee and the sharer wants to leave or abort
+				InviteeEngine engine = new InviteeEngine();
+				processStateUpdate(txn, m.getId(),
+						engine.onMessageReceived(state, msg));
+			}
+		} else {
+			// message has passed validator, so that should never happen
+			throw new RuntimeException("Illegal Forum Sharing Message");
+		}
+	}
+
+	@Override
+	public ClientId getClientId() {
+		return CLIENT_ID;
+	}
+
+	@Override
+	public void sendForumInvitation(GroupId groupId, ContactId contactId,
+			String msg) throws DbException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			// initialize local state for sharer
+			Forum f = forumManager.getForum(txn, groupId);
+			BdfDictionary localState = initializeSharerState(txn, f, contactId);
+
+			// define action
+			BdfDictionary localAction = new BdfDictionary();
+			localAction.put(TYPE, SHARE_MSG_TYPE_INVITATION);
+			if (!StringUtils.isNullOrEmpty(msg)) {
+				localAction.put(INVITATION_MSG, msg);
+			}
+
+			// start engine and process its state update
+			SharerEngine engine = new SharerEngine();
+			processStateUpdate(txn, null,
+					engine.onLocalAction(localState, localAction));
+
+			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException();
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	@Override
+	public void respondToInvitation(Forum f, boolean accept)
 			throws DbException {
+
+		Transaction txn = db.startTransaction(false);
 		try {
-			ContactId contactId = getContactId(txn, m.getGroupId());
-			setForumVisibility(txn, contactId, getVisibleForums(txn, m));
+			// find session state based on forum
+			BdfDictionary localState = getSessionStateForResponse(txn, f);
+
+			// define action
+			BdfDictionary localAction = new BdfDictionary();
+			if (accept) {
+				localAction.put(TYPE, SHARE_MSG_TYPE_ACCEPT);
+			} else {
+				localAction.put(TYPE, SHARE_MSG_TYPE_DECLINE);
+			}
+
+			// start engine and process its state update
+			InviteeEngine engine = new InviteeEngine();
+			processStateUpdate(txn, null,
+					engine.onLocalAction(localState, localAction));
+
+			txn.setComplete();
 		} catch (FormatException e) {
 			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
 		}
 	}
 
 	@Override
-	public ClientId getClientId() {
-		return CLIENT_ID;
+	public Collection<ForumInvitationMessage> getForumInvitationMessages(
+			ContactId contactId) throws DbException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			Contact contact = db.getContact(txn, contactId);
+			Group group = getContactGroup(contact);
+
+			Collection<ForumInvitationMessage> list =
+					new ArrayList<ForumInvitationMessage>();
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, group.getId());
+			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+				BdfDictionary msg = m.getValue();
+				try {
+					if (msg.getLong(TYPE) != SHARE_MSG_TYPE_INVITATION)
+						continue;
+
+					MessageStatus status =
+							db.getMessageStatus(txn, contactId, m.getKey());
+					SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
+					String name = msg.getString(FORUM_NAME);
+					String message = msg.getOptionalString(INVITATION_MSG);
+					long time = msg.getLong(TIME);
+					boolean local = msg.getBoolean(LOCAL);
+					boolean read = msg.getBoolean(READ, false);
+					boolean available = false;
+					if (!local) {
+						// figure out whether the forum is still available
+						BdfDictionary sessionState =
+								getSessionState(txn, sessionId, true);
+						InviteeProtocolState state = InviteeProtocolState
+								.fromValue(
+										sessionState.getLong(STATE).intValue());
+						available = state == AWAIT_LOCAL_RESPONSE;
+					}
+					ForumInvitationMessage im =
+							new ForumInvitationMessage(m.getKey(), sessionId,
+									contactId, name, message, available, time,
+									local, status.isSent(), status.isSeen(),
+									read);
+					list.add(im);
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+			txn.setComplete();
+			return list;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
 	}
 
 	@Override
 	public void removingForum(Transaction txn, Forum f) throws DbException {
 		try {
-			for (Contact c : db.getContacts(txn))
-				removeFromList(txn, getContactGroup(c).getId(), f);
+			for (Contact c : db.getContacts(txn)) {
+				GroupId g = getContactGroup(c).getId();
+				if (removeFromList(txn, g, TO_BE_SHARED_BY_US, f)) {
+					leaveForum(txn, c.getId(), f);
+				}
+				if (removeFromList(txn, g, SHARED_BY_US, f)) {
+					leaveForum(txn, c.getId(), f);
+				}
+				if (removeFromList(txn, g, SHARED_WITH_US, f)) {
+					leaveForum(txn, c.getId(), f);
+				}
+			}
 		} catch (IOException e) {
 			throw new DbException(e);
 		}
@@ -135,16 +402,11 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
 				// Get all forums shared by contacts
 				for (Contact c : db.getContacts(txn)) {
 					Group g = getContactGroup(c);
-					// Find the latest update version
-					LatestUpdate latest = findLatest(txn, g.getId(), false);
-					if (latest != null) {
-						// Retrieve and parse the latest update
-						BdfList message = clientHelper.getMessageAsList(txn,
-								latest.messageId);
-						for (Forum f : parseForumList(message)) {
-							if (!subscribed.contains(f.getGroup()))
-								available.add(f);
-						}
+					List<Forum> forums =
+							getForumList(txn, g.getId(), SHARED_WITH_US);
+					for (Forum f : forums) {
+						if (!subscribed.contains(f.getGroup()))
+							available.add(f);
 					}
 				}
 				txn.setComplete();
@@ -164,7 +426,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
 			Transaction txn = db.startTransaction(true);
 			try {
 				for (Contact c : db.getContacts(txn)) {
-					if (listContains(txn, getContactGroup(c).getId(), g, false))
+					GroupId contactGroup = getContactGroup(c).getId();
+					if (listContains(txn, contactGroup, g, SHARED_WITH_US))
 						subscribers.add(c);
 				}
 				txn.setComplete();
@@ -184,7 +447,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
 			Transaction txn = db.startTransaction(true);
 			try {
 				for (Contact c : db.getContacts(txn)) {
-					if (listContains(txn, getContactGroup(c).getId(), g, true))
+					GroupId contactGroup = getContactGroup(c).getId();
+					if (listContains(txn, contactGroup, g, SHARED_BY_US))
 						shared.add(c.getId());
 				}
 				txn.setComplete();
@@ -198,178 +462,417 @@ class ForumSharingManagerImpl implements ForumSharingManager, Client,
 	}
 
 	@Override
-	public void setSharedWith(GroupId g, Collection<ContactId> shared)
+	public boolean canBeShared(GroupId g, Contact c) throws DbException {
+		boolean canBeShared;
+		Transaction txn = db.startTransaction(true);
+		try {
+			canBeShared = canBeShared(txn, g, c);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return canBeShared;
+	}
+
+	private boolean canBeShared(Transaction txn, GroupId g, Contact c)
 			throws DbException {
+
+		try {
+			GroupId contactGroup = getContactGroup(c).getId();
+			return !listContains(txn, contactGroup, g, SHARED_BY_US) &&
+					!listContains(txn, contactGroup, g, SHARED_WITH_US) &&
+					!listContains(txn, contactGroup, g, TO_BE_SHARED_BY_US);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private BdfDictionary initializeSharerState(Transaction txn, Forum f,
+			ContactId contactId) throws FormatException, DbException {
+
+		Contact c = db.getContact(txn, contactId);
+		Group group = getContactGroup(c);
+
+		// create local message to keep engine state
+		long now = clock.currentTimeMillis();
+		Bytes salt = new Bytes(new byte[FORUM_SALT_LENGTH]);
+		random.nextBytes(salt.getBytes());
+		Message m = clientHelper.createMessage(localGroup.getId(), now,
+				BdfList.of(salt));
+		MessageId sessionId = m.getId();
+
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_ID, sessionId);
+		d.put(STORAGE_ID, sessionId);
+		d.put(GROUP_ID, group.getId());
+		d.put(IS_SHARER, true);
+		d.put(STATE, PREPARE_INVITATION.getValue());
+		d.put(CONTACT_ID, contactId.getInt());
+		d.put(FORUM_ID, f.getId());
+		d.put(FORUM_NAME, f.getName());
+		d.put(FORUM_SALT, f.getSalt());
+
+		// save local state to database
+		clientHelper.addLocalMessage(txn, m, getClientId(), d, false);
+
+		return d;
+	}
+
+	private BdfDictionary initializeInviteeState(Transaction txn,
+			ContactId contactId, BdfDictionary msg)
+			throws FormatException, DbException {
+
+		Contact c = db.getContact(txn, contactId);
+		Group group = getContactGroup(c);
+		String name = msg.getString(FORUM_NAME);
+		byte[] salt = msg.getRaw(FORUM_SALT);
+		Forum f = forumManager.createForum(name, salt);
+
+		// create local message to keep engine state
+		long now = clock.currentTimeMillis();
+		Bytes mSalt = new Bytes(new byte[FORUM_SALT_LENGTH]);
+		random.nextBytes(mSalt.getBytes());
+		Message m = clientHelper.createMessage(localGroup.getId(), now,
+				BdfList.of(mSalt));
+
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_ID, msg.getRaw(SESSION_ID));
+		d.put(STORAGE_ID, m.getId());
+		d.put(GROUP_ID, group.getId());
+		d.put(IS_SHARER, false);
+		d.put(STATE, AWAIT_INVITATION.getValue());
+		d.put(CONTACT_ID, contactId.getInt());
+		d.put(FORUM_ID, f.getId());
+		d.put(FORUM_NAME, name);
+		d.put(FORUM_SALT, salt);
+
+		// save local state to database
+		clientHelper.addLocalMessage(txn, m, getClientId(), d, false);
+
+		return d;
+	}
+
+	private BdfDictionary getSessionState(Transaction txn, SessionId sessionId,
+			boolean warn) throws DbException, FormatException {
+
 		try {
-			Transaction txn = db.startTransaction(false);
+			// we should be able to get the sharer state directly from sessionId
+			return clientHelper.getMessageMetadataAsDictionary(txn, sessionId);
+		} catch (NoSuchMessageException e) {
+			// State not found directly, so iterate over all states
+			// to find state for invitee
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId());
+			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+				BdfDictionary state = m.getValue();
+				if (Arrays.equals(state.getRaw(SESSION_ID),
+						sessionId.getBytes())) {
+					return state;
+				}
+			}
+			if (warn && LOG.isLoggable(WARNING)) {
+				LOG.warning(
+						"No session state found for message with session ID " +
+								Arrays.hashCode(sessionId.getBytes()));
+			}
+			throw new FormatException();
+		}
+	}
+
+	private BdfDictionary getSessionStateForResponse(Transaction txn, Forum f)
+			throws DbException, FormatException {
+
+		Map<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId());
+		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+			BdfDictionary d = m.getValue();
 			try {
-				// Retrieve the forum
-				Forum f = parseForum(db.getGroup(txn, g));
-				// Update the list shared with each contact
-				shared = new HashSet<ContactId>(shared);
-				for (Contact c : db.getContacts(txn)) {
-					Group cg = getContactGroup(c);
-					if (shared.contains(c.getId())) {
-						if (addToList(txn, cg.getId(), f)) {
-							if (listContains(txn, cg.getId(), g, false))
-								db.setVisibleToContact(txn, c.getId(), g, true);
-						}
-					} else {
-						removeFromList(txn, cg.getId(), f);
-						db.setVisibleToContact(txn, c.getId(), g, false);
+				InviteeProtocolState state = InviteeProtocolState
+						.fromValue(d.getLong(STATE).intValue());
+				if (state == AWAIT_LOCAL_RESPONSE) {
+					byte[] id = d.getRaw(FORUM_ID);
+					if (Arrays.equals(f.getId().getBytes(), id)) {
+						// Note that there should always be only one session
+						// in this state for the same forum
+						return d;
 					}
 				}
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
-		} catch (FormatException e) {
-			throw new DbException(e);
 		}
+		throw new DbException();
 	}
 
-	private Group getContactGroup(Contact c) {
-		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	private BdfDictionary getSessionStateForLeaving(Transaction txn, Forum f,
+			ContactId c) throws DbException, FormatException {
+
+		Map<MessageId, BdfDictionary> map = clientHelper
+				.getMessageMetadataAsDictionary(txn, localGroup.getId());
+		for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+			BdfDictionary d = m.getValue();
+			try {
+				// check that this session is with the right contact
+				if (c.getInt() != d.getLong(CONTACT_ID)) continue;
+				// check that a forum get be left in current session
+				int intState = d.getLong(STATE).intValue();
+				if (d.getBoolean(IS_SHARER)) {
+					SharerProtocolState state =
+							SharerProtocolState.fromValue(intState);
+					if (state.next(SharerAction.LOCAL_LEAVE) ==
+							SharerProtocolState.ERROR) continue;
+				} else {
+					InviteeProtocolState state = InviteeProtocolState
+							.fromValue(intState);
+					if (state.next(InviteeAction.LOCAL_LEAVE) ==
+							InviteeProtocolState.ERROR) continue;
+				}
+				// check that this state actually concerns this forum
+				String name = d.getString(FORUM_NAME);
+				byte[] salt = d.getRaw(FORUM_SALT);
+				if (name.equals(f.getName()) &&
+						Arrays.equals(salt, f.getSalt())) {
+					// TODO what happens when there is more than one invitation?
+					return d;
+				}
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			}
+		}
+		throw new FormatException();
 	}
 
-	private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local)
+	private void processStateUpdate(Transaction txn, MessageId messageId,
+			StateUpdate<BdfDictionary, BdfDictionary> result)
 			throws DbException, FormatException {
-		LatestUpdate latest = null;
-		Map<MessageId, BdfDictionary> metadata =
-				clientHelper.getMessageMetadataAsDictionary(txn, g);
-		for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
-			BdfDictionary meta = e.getValue();
-			if (meta.getBoolean("local") != local) continue;
-			long version = meta.getLong("version");
-			if (latest == null || version > latest.version)
-				latest = new LatestUpdate(e.getKey(), version);
+
+		// perform actions based on new local state
+		performTasks(txn, result.localState);
+
+		// save new local state
+		MessageId storageId =
+				new MessageId(result.localState.getRaw(STORAGE_ID));
+		clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
+
+		// send messages
+		for (BdfDictionary d : result.toSend) {
+			sendMessage(txn, d);
+		}
+
+		// broadcast events
+		for (Event event : result.toBroadcast) {
+			txn.attach(event);
+		}
+
+		// delete message
+		if (result.deleteMessage && messageId != null) {
+			if (LOG.isLoggable(INFO)) {
+				LOG.info("Deleting message with id " + messageId.hashCode());
+			}
+			db.deleteMessage(txn, messageId);
+			db.deleteMessageMetadata(txn, messageId);
 		}
-		return latest;
 	}
 
-	private List<Forum> parseForumList(BdfList message) throws FormatException {
-		// Version, forum list
-		BdfList forumList = message.getList(1);
-		List<Forum> forums = new ArrayList<Forum>(forumList.size());
-		for (int i = 0; i < forumList.size(); i++) {
-			// Name, salt
-			BdfList forum = forumList.getList(i);
-			forums.add(forumManager
-					.createForum(forum.getString(0), forum.getRaw(1)));
+	private void performTasks(Transaction txn, BdfDictionary localState)
+			throws FormatException, DbException {
+
+		if (!localState.containsKey(TASK)) return;
+
+		// remember task and remove it from localState
+		long task = localState.getLong(TASK);
+		localState.put(TASK, BdfDictionary.NULL_VALUE);
+
+		// get group ID for later
+		GroupId groupId = new GroupId(localState.getRaw(GROUP_ID));
+		// get contact ID for later
+		ContactId contactId =
+				new ContactId(localState.getLong(CONTACT_ID).intValue());
+
+		// get forum for later
+		String name = localState.getString(FORUM_NAME);
+		byte[] salt = localState.getRaw(FORUM_SALT);
+		Forum f = forumManager.createForum(name, salt);
+
+		// perform tasks
+		if (task == TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US) {
+			addToList(txn, groupId, SHARED_WITH_US, f);
+		}
+		else if (task == TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US) {
+			removeFromList(txn, groupId, SHARED_WITH_US, f);
+		}
+		else if (task == TASK_ADD_SHARED_FORUM) {
+			db.addGroup(txn, f.getGroup());
+			db.setVisibleToContact(txn, contactId, f.getId(), true);
+		}
+		else if (task == TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US) {
+			addToList(txn, groupId, TO_BE_SHARED_BY_US, f);
+		}
+		else if (task == TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US) {
+			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
+		}
+		else if (task == TASK_SHARE_FORUM) {
+			db.setVisibleToContact(txn, contactId, f.getId(), true);
+			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
+			addToList(txn, groupId, SHARED_BY_US, f);
+		}
+		else if (task == TASK_UNSHARE_FORUM_SHARED_BY_US) {
+			db.setVisibleToContact(txn, contactId, f.getId(), false);
+			removeFromList(txn, groupId, SHARED_BY_US, f);
+		} else if (task == TASK_UNSHARE_FORUM_SHARED_WITH_US) {
+			db.setVisibleToContact(txn, contactId, f.getId(), false);
+			removeFromList(txn, groupId, SHARED_WITH_US, f);
 		}
-		return forums;
 	}
 
-	private void storeMessage(Transaction txn, GroupId g, List<Forum> forums,
-			long version) throws DbException {
-		try {
-			BdfList body = encodeForumList(forums, version);
-			long now = clock.currentTimeMillis();
-			Message m = clientHelper.createMessage(g, now, body);
-			BdfDictionary meta = new BdfDictionary();
-			meta.put("version", version);
-			meta.put("local", true);
-			clientHelper.addLocalMessage(txn, m, CLIENT_ID, meta, true);
-		} catch (FormatException e) {
-			throw new RuntimeException(e);
+	private void sendMessage(Transaction txn, BdfDictionary m)
+			throws FormatException, DbException {
+
+		BdfList list = encodeMessage(m);
+		byte[] body = clientHelper.toByteArray(list);
+		GroupId groupId = new GroupId(m.getRaw(GROUP_ID));
+		Group group = db.getGroup(txn, groupId);
+		long timestamp = clock.currentTimeMillis();
+
+		// add message itself as metadata
+		m.put(LOCAL, true);
+		m.put(TIME, timestamp);
+		Metadata meta = metadataEncoder.encode(m);
+
+		messageQueueManager
+				.sendMessage(txn, group, timestamp, body, meta);
+	}
+
+	private BdfList encodeMessage(BdfDictionary m) throws FormatException {
+		long type = m.getLong(TYPE);
+
+		BdfList list;
+		if (type == SHARE_MSG_TYPE_INVITATION) {
+			list = BdfList.of(type,
+					m.getRaw(SESSION_ID),
+					m.getString(FORUM_NAME),
+					m.getRaw(FORUM_SALT)
+			);
+			String msg = m.getOptionalString(INVITATION_MSG);
+			if (msg != null) list.add(msg);
+		} else if (type == SHARE_MSG_TYPE_ACCEPT) {
+			list = BdfList.of(type, m.getRaw(SESSION_ID));
+		} else if (type == SHARE_MSG_TYPE_DECLINE) {
+			list = BdfList.of(type, m.getRaw(SESSION_ID));
+		} else if (type == SHARE_MSG_TYPE_LEAVE) {
+			list = BdfList.of(type, m.getRaw(SESSION_ID));
+		} else if (type == SHARE_MSG_TYPE_ABORT) {
+			list = BdfList.of(type, m.getRaw(SESSION_ID));
+		} else {
+			throw new FormatException();
 		}
+		return list;
 	}
 
-	private BdfList encodeForumList(List<Forum> forums, long version) {
-		BdfList forumList = new BdfList();
-		for (Forum f : forums)
-			forumList.add(BdfList.of(f.getName(), f.getSalt()));
-		return BdfList.of(version, forumList);
+	private Group getContactGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
 	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
 			throws DbException, FormatException {
 		BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
 				contactGroupId);
-		return new ContactId(meta.getLong("contactId").intValue());
-	}
-
-	private Set<GroupId> getVisibleForums(Transaction txn,
-			Message remoteUpdate) throws DbException, FormatException {
-		// Get the latest local update
-		LatestUpdate local = findLatest(txn, remoteUpdate.getGroupId(), true);
-		// If there's no local update, no forums are visible
-		if (local == null) return Collections.emptySet();
-		// Intersect the sets of shared forums
-		BdfList localMessage = clientHelper.getMessageAsList(txn,
-				local.messageId);
-		Set<Forum> shared = new HashSet<Forum>(parseForumList(localMessage));
-		byte[] raw = remoteUpdate.getRaw();
-		BdfList remoteMessage = clientHelper.toList(raw, MESSAGE_HEADER_LENGTH,
-				raw.length - MESSAGE_HEADER_LENGTH);
-		shared.retainAll(parseForumList(remoteMessage));
-		// Forums in the intersection should be visible
-		Set<GroupId> visible = new HashSet<GroupId>(shared.size());
-		for (Forum f : shared) visible.add(f.getId());
-		return visible;
-	}
-
-	private void setForumVisibility(Transaction txn, ContactId c,
-			Set<GroupId> visible) throws DbException {
-		for (Group g : db.getGroups(txn, forumManager.getClientId())) {
-			boolean isVisible = db.isVisibleToContact(txn, c, g.getId());
-			boolean shouldBeVisible = visible.contains(g.getId());
-			if (isVisible && !shouldBeVisible)
-				db.setVisibleToContact(txn, c, g.getId(), false);
-			else if (!isVisible && shouldBeVisible)
-				db.setVisibleToContact(txn, c, g.getId(), true);
-		}
+		return new ContactId(meta.getLong(CONTACT_ID).intValue());
 	}
 
-	private Forum parseForum(Group g) throws FormatException {
-		byte[] descriptor = g.getDescriptor();
-		// Name, salt
-		BdfList forum = clientHelper.toList(descriptor, 0, descriptor.length);
-		return new Forum(g, forum.getString(0), forum.getRaw(1));
+	private void leaveForum(Transaction txn, ContactId c, Forum f)
+			throws DbException, FormatException {
+
+		BdfDictionary state = getSessionStateForLeaving(txn, f, c);
+		BdfDictionary action = new BdfDictionary();
+		action.put(TYPE, SHARE_MSG_TYPE_LEAVE);
+		if (state.getBoolean(IS_SHARER)) {
+			SharerEngine engine = new SharerEngine();
+			processStateUpdate(txn, null,
+					engine.onLocalAction(state, action));
+		} else {
+			InviteeEngine engine = new InviteeEngine();
+			processStateUpdate(txn, null,
+					engine.onLocalAction(state, action));
+		}
 	}
 
-	private boolean listContains(Transaction txn, GroupId g, GroupId forum,
-			boolean local) throws DbException, FormatException {
-		LatestUpdate latest = findLatest(txn, g, local);
-		if (latest == null) return false;
-		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
-		List<Forum> list = parseForumList(message);
-		for (Forum f : list) if (f.getId().equals(forum)) return true;
+	private boolean listContains(Transaction txn, GroupId contactGroup,
+			GroupId forum, String key) throws DbException, FormatException {
+
+		List<Forum> list = getForumList(txn, contactGroup, key);
+		for (Forum f : list) {
+			if (f.getId().equals(forum)) return true;
+		}
 		return false;
 	}
 
-	private boolean addToList(Transaction txn, GroupId g, Forum f)
-			throws DbException, FormatException {
-		LatestUpdate latest = findLatest(txn, g, true);
-		if (latest == null) {
-			storeMessage(txn, g, Collections.singletonList(f), 0);
+	private boolean addToList(Transaction txn, GroupId groupId, String key,
+			Forum f) throws DbException, FormatException {
+
+		List<Forum> forums = getForumList(txn, groupId, key);
+		if (forums.contains(f)) return false;
+		forums.add(f);
+		storeForumList(txn, groupId, key, forums);
+		return true;
+	}
+
+	private boolean removeFromList(Transaction txn, GroupId groupId, String key,
+			Forum f) throws DbException, FormatException {
+
+		List<Forum> forums = getForumList(txn, groupId, key);
+		if (forums.remove(f)) {
+			storeForumList(txn, groupId, key, forums);
 			return true;
 		}
-		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
-		List<Forum> list = parseForumList(message);
-		if (list.contains(f)) return false;
-		list.add(f);
-		storeMessage(txn, g, list, latest.version + 1);
-		return true;
+		return false;
 	}
 
-	private void removeFromList(Transaction txn, GroupId g, Forum f)
-			throws DbException, FormatException {
-		LatestUpdate latest = findLatest(txn, g, true);
-		if (latest == null) return;
-		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
-		List<Forum> list = parseForumList(message);
-		if (list.remove(f)) storeMessage(txn, g, list, latest.version + 1);
+	private List<Forum> getForumList(Transaction txn, GroupId groupId,
+			String key) throws DbException, FormatException {
+
+		BdfDictionary metadata =
+				clientHelper.getGroupMetadataAsDictionary(txn, groupId);
+		BdfList list = metadata.getList(key);
+
+		return parseForumList(list);
 	}
 
-	private static class LatestUpdate {
+	private void storeForumList(Transaction txn, GroupId groupId, String key,
+			List<Forum> forums)	throws DbException, FormatException {
+
+		BdfList list = encodeForumList(forums);
+		BdfDictionary metadata = BdfDictionary.of(
+				new BdfEntry(key, list)
+		);
+		clientHelper.mergeGroupMetadata(txn, groupId, metadata);
+	}
 
-		private final MessageId messageId;
-		private final long version;
+	private BdfList encodeForumList(List<Forum> forums) {
+		BdfList forumList = new BdfList();
+		for (Forum f : forums)
+			forumList.add(BdfList.of(f.getName(), f.getSalt()));
+		return forumList;
+	}
 
-		private LatestUpdate(MessageId messageId, long version) {
-			this.messageId = messageId;
-			this.version = version;
+	private List<Forum> parseForumList(BdfList list) throws FormatException {
+		List<Forum> forums = new ArrayList<Forum>(list.size());
+		for (int i = 0; i < list.size(); i++) {
+			BdfList forum = list.getList(i);
+			forums.add(forumManager
+					.createForum(forum.getString(0), forum.getRaw(1)));
 		}
+		return forums;
 	}
+
+	private void deleteMessage(Transaction txn, MessageId messageId)
+			throws DbException {
+
+		if (LOG.isLoggable(INFO))
+			LOG.info("Deleting message with ID: " + messageId.hashCode());
+
+		db.deleteMessage(txn, messageId);
+		db.deleteMessageMetadata(txn, messageId);
+	}
+
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumSharingValidator.java b/briar-core/src/org/briarproject/forum/ForumSharingValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..c204623a27dee2dc193befcaaad12ec915a12cba
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/ForumSharingValidator.java
@@ -0,0 +1,81 @@
+package org.briarproject.forum;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.system.Clock;
+import org.briarproject.clients.BdfMessageValidator;
+
+import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
+import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
+import static org.briarproject.api.forum.ForumConstants.INVITATION_MSG;
+import static org.briarproject.api.forum.ForumConstants.LOCAL;
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
+import static org.briarproject.api.forum.ForumConstants.SESSION_ID;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.api.forum.ForumConstants.TIME;
+import static org.briarproject.api.forum.ForumConstants.TYPE;
+import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
+
+class ForumSharingValidator extends BdfMessageValidator {
+
+	ForumSharingValidator(ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+	}
+
+	@Override
+	protected BdfDictionary validateMessage(Message m, Group g,
+			BdfList body) throws FormatException {
+
+		BdfDictionary d = new BdfDictionary();
+		long type = body.getLong(0);
+		byte[] id = body.getRaw(1);
+		checkLength(id, SessionId.LENGTH);
+
+		if (type == SHARE_MSG_TYPE_INVITATION) {
+			checkSize(body, 4, 5);
+
+			String name = body.getString(2);
+			checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
+
+			byte[] salt = body.getRaw(3);
+			checkLength(salt, FORUM_SALT_LENGTH);
+
+			d.put(FORUM_NAME, name);
+			d.put(FORUM_SALT, salt);
+
+			if (body.size() > 4) {
+				String msg = body.getString(4);
+				checkLength(msg, 0, MAX_MESSAGE_BODY_LENGTH);
+				d.put(INVITATION_MSG, msg);
+			}
+		} else {
+			checkSize(body, 2);
+			if (type != SHARE_MSG_TYPE_ACCEPT &&
+					type != SHARE_MSG_TYPE_DECLINE &&
+					type != SHARE_MSG_TYPE_LEAVE &&
+					type != SHARE_MSG_TYPE_ABORT) {
+				throw new FormatException();
+			}
+		}
+		// Return the metadata
+		d.put(TYPE, type);
+		d.put(SESSION_ID, id);
+		d.put(GROUP_ID, m.getGroupId());
+		d.put(LOCAL, false);
+		d.put(TIME, m.getTimestamp());
+		return d;
+	}
+}
diff --git a/briar-core/src/org/briarproject/forum/InviteeEngine.java b/briar-core/src/org/briarproject/forum/InviteeEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..f208204db66386c080a5511a2ed75445879583e3
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/InviteeEngine.java
@@ -0,0 +1,265 @@
+package org.briarproject.forum;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ProtocolEngine;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfEntry;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.ForumInvitationReceivedEvent;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.InviteeAction;
+import org.briarproject.api.forum.InviteeProtocolState;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.forum.ForumConstants.CONTACT_ID;
+import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
+import static org.briarproject.api.forum.ForumConstants.SESSION_ID;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.api.forum.ForumConstants.STATE;
+import static org.briarproject.api.forum.ForumConstants.TASK;
+import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_ADD_SHARED_FORUM;
+import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_WITH_US;
+import static org.briarproject.api.forum.ForumConstants.TYPE;
+import static org.briarproject.api.forum.InviteeAction.LOCAL_ABORT;
+import static org.briarproject.api.forum.InviteeAction.LOCAL_ACCEPT;
+import static org.briarproject.api.forum.InviteeAction.LOCAL_DECLINE;
+import static org.briarproject.api.forum.InviteeAction.LOCAL_LEAVE;
+import static org.briarproject.api.forum.InviteeAction.REMOTE_INVITATION;
+import static org.briarproject.api.forum.InviteeAction.REMOTE_LEAVE;
+import static org.briarproject.api.forum.InviteeProtocolState.ERROR;
+import static org.briarproject.api.forum.InviteeProtocolState.FINISHED;
+import static org.briarproject.api.forum.InviteeProtocolState.LEFT;
+
+public class InviteeEngine
+		implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
+
+	private static final Logger LOG =
+			Logger.getLogger(SharerEngine.class.getName());
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
+			BdfDictionary localState, BdfDictionary localAction) {
+
+		try {
+			InviteeProtocolState currentState =
+					getState(localState.getLong(STATE));
+			long type = localAction.getLong(TYPE);
+			InviteeAction action = InviteeAction.getLocal(type);
+			InviteeProtocolState nextState = currentState.next(action);
+			localState.put(STATE, nextState.getValue());
+
+			if (action == LOCAL_ABORT && currentState != ERROR) {
+				return abortSession(currentState, localState);
+			}
+
+			if (nextState == ERROR) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Error: Invalid action in state " +
+							currentState.name());
+				}
+				return noUpdate(localState, true);
+			}
+			List<BdfDictionary> messages;
+			List<Event> events = Collections.emptyList();
+
+			if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) {
+				BdfDictionary msg = BdfDictionary.of(
+						new BdfEntry(SESSION_ID, localState.getRaw(SESSION_ID)),
+						new BdfEntry(GROUP_ID, localState.getRaw(GROUP_ID))
+				);
+				if (action == LOCAL_ACCEPT) {
+					localState.put(TASK, TASK_ADD_SHARED_FORUM);
+					msg.put(TYPE, SHARE_MSG_TYPE_ACCEPT);
+				} else {
+					localState.put(TASK,
+							TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US);
+					msg.put(TYPE, SHARE_MSG_TYPE_DECLINE);
+				}
+				messages = Collections.singletonList(msg);
+				logLocalAction(currentState, localState, msg);
+			}
+			else if (action == LOCAL_LEAVE) {
+				BdfDictionary msg = new BdfDictionary();
+				msg.put(TYPE, SHARE_MSG_TYPE_LEAVE);
+				msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+				msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+				messages = Collections.singletonList(msg);
+				logLocalAction(currentState, localState, msg);
+			}
+			else {
+				throw new IllegalArgumentException("Unknown Local Action");
+			}
+			return new StateUpdate<BdfDictionary, BdfDictionary>(false,
+					false, localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
+			BdfDictionary localState, BdfDictionary msg) {
+
+		try {
+			InviteeProtocolState currentState =
+					getState(localState.getLong(STATE));
+			long type = msg.getLong(TYPE);
+			InviteeAction action = InviteeAction.getRemote(type);
+			InviteeProtocolState nextState = currentState.next(action);
+			localState.put(STATE, nextState.getValue());
+
+			logMessageReceived(currentState, nextState, type, msg);
+
+			if (nextState == ERROR) {
+				if (currentState != ERROR) {
+					return abortSession(currentState, localState);
+				} else {
+					return noUpdate(localState, true);
+				}
+			}
+
+			List<BdfDictionary> messages = Collections.emptyList();
+			List<Event> events = Collections.emptyList();
+			boolean deleteMsg = false;
+
+			if (currentState == LEFT) {
+				// ignore and delete messages coming in while in that state
+				deleteMsg = true;
+			}
+			// the sharer left the forum she had shared with us
+			else if (action == REMOTE_LEAVE && currentState == FINISHED) {
+				localState.put(TASK, TASK_UNSHARE_FORUM_SHARED_WITH_US);
+			}
+			else if (currentState == FINISHED) {
+				// ignore and delete messages coming in while in that state
+				// note that LEAVE is possible, but was handled above
+				deleteMsg = true;
+			}
+			// the sharer left the forum before we couldn't even respond
+			else if (action == REMOTE_LEAVE) {
+				localState.put(TASK, TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US);
+			}
+			// we have just received our invitation
+			else if (action == REMOTE_INVITATION) {
+				localState.put(TASK, TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US);
+				// TODO how to get the proper group here?
+				Forum forum = new Forum(null, localState.getString(FORUM_NAME),
+						localState.getRaw(FORUM_SALT));
+				ContactId contactId = new ContactId(
+						localState.getLong(CONTACT_ID).intValue());
+				Event event = new ForumInvitationReceivedEvent(forum, contactId);
+				events = Collections.singletonList(event);
+			}
+			else {
+				throw new IllegalArgumentException("Bad state");
+			}
+			return new StateUpdate<BdfDictionary, BdfDictionary>(deleteMsg,
+					false, localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	private void logLocalAction(InviteeProtocolState state,
+			BdfDictionary localState, BdfDictionary msg) {
+
+		if (!LOG.isLoggable(INFO)) return;
+
+		String a = "response";
+		if (msg.getLong(TYPE, -1L) == SHARE_MSG_TYPE_LEAVE) a = "leave";
+
+		try {
+			LOG.info("Sending " + a + " in state " + state.name() +
+					" with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " +
+					getState(localState.getLong(STATE)).name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private void logMessageReceived(InviteeProtocolState currentState,
+			InviteeProtocolState nextState, long type, BdfDictionary msg) {
+		if (!LOG.isLoggable(INFO)) return;
+
+		try {
+			String t = "unknown";
+			if (type == SHARE_MSG_TYPE_INVITATION) t = "INVITE";
+			else if (type == SHARE_MSG_TYPE_LEAVE) t = "LEAVE";
+			else if (type == SHARE_MSG_TYPE_ABORT) t = "ABORT";
+
+			LOG.info("Received " + t + " in state " + currentState.name() +
+					" with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " + nextState.name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
+			BdfDictionary localState, BdfDictionary delivered) {
+		try {
+			return noUpdate(localState, false);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return null;
+		}
+	}
+
+	private InviteeProtocolState getState(Long state) {
+		return InviteeProtocolState.fromValue(state.intValue());
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
+			InviteeProtocolState currentState, BdfDictionary localState)
+			throws FormatException {
+
+		if (LOG.isLoggable(WARNING)) {
+			LOG.warning("Aborting protocol session " +
+					Arrays.hashCode(localState.getRaw(SESSION_ID)) +
+					" in state " + currentState.name());
+		}
+
+		localState.put(STATE, ERROR.getValue());
+		BdfDictionary msg = new BdfDictionary();
+		msg.put(TYPE, SHARE_MSG_TYPE_ABORT);
+		msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+		msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+		List<BdfDictionary> messages = Collections.singletonList(msg);
+
+		List<Event> events = Collections.emptyList();
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, messages, events);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
+			BdfDictionary localState, boolean delete) throws FormatException {
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(delete, false,
+				localState, Collections.<BdfDictionary>emptyList(),
+				Collections.<Event>emptyList());
+	}
+}
diff --git a/briar-core/src/org/briarproject/forum/SharerEngine.java b/briar-core/src/org/briarproject/forum/SharerEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..05db965b80fb8f78aaa7b86c3f67cae3a838759f
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/SharerEngine.java
@@ -0,0 +1,264 @@
+package org.briarproject.forum;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ProtocolEngine;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.ForumInvitationResponseReceivedEvent;
+import org.briarproject.api.forum.SharerAction;
+import org.briarproject.api.forum.SharerProtocolState;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.forum.ForumConstants.CONTACT_ID;
+import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
+import static org.briarproject.api.forum.ForumConstants.INVITATION_MSG;
+import static org.briarproject.api.forum.ForumConstants.SESSION_ID;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.api.forum.ForumConstants.STATE;
+import static org.briarproject.api.forum.ForumConstants.TASK;
+import static org.briarproject.api.forum.ForumConstants.TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TASK_SHARE_FORUM;
+import static org.briarproject.api.forum.ForumConstants.TASK_UNSHARE_FORUM_SHARED_BY_US;
+import static org.briarproject.api.forum.ForumConstants.TYPE;
+import static org.briarproject.api.forum.SharerAction.LOCAL_ABORT;
+import static org.briarproject.api.forum.SharerAction.LOCAL_INVITATION;
+import static org.briarproject.api.forum.SharerAction.LOCAL_LEAVE;
+import static org.briarproject.api.forum.SharerAction.REMOTE_ACCEPT;
+import static org.briarproject.api.forum.SharerAction.REMOTE_DECLINE;
+import static org.briarproject.api.forum.SharerAction.REMOTE_LEAVE;
+import static org.briarproject.api.forum.SharerProtocolState.ERROR;
+import static org.briarproject.api.forum.SharerProtocolState.FINISHED;
+import static org.briarproject.api.forum.SharerProtocolState.LEFT;
+
+public class SharerEngine
+		implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
+
+	private static final Logger LOG =
+			Logger.getLogger(SharerEngine.class.getName());
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
+			BdfDictionary localState, BdfDictionary localAction) {
+
+		try {
+			SharerProtocolState currentState =
+					getState(localState.getLong(STATE));
+			long type = localAction.getLong(TYPE);
+			SharerAction action = SharerAction.getLocal(type);
+			SharerProtocolState nextState = currentState.next(action);
+			localState.put(STATE, nextState.getValue());
+
+			if (action == LOCAL_ABORT && currentState != ERROR) {
+				return abortSession(currentState, localState);
+			}
+
+			if (nextState == ERROR) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Error: Invalid action in state " +
+							currentState.name());
+				}
+				return noUpdate(localState, true);
+			}
+			List<BdfDictionary> messages;
+			List<Event> events = Collections.emptyList();
+
+			if (action == LOCAL_INVITATION) {
+				BdfDictionary msg = new BdfDictionary();
+				msg.put(TYPE, SHARE_MSG_TYPE_INVITATION);
+				msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+				msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+				msg.put(FORUM_NAME, localState.getString(FORUM_NAME));
+				msg.put(FORUM_SALT, localState.getRaw(FORUM_SALT));
+				if (localAction.containsKey(INVITATION_MSG)) {
+					msg.put(INVITATION_MSG,
+							localAction.getString(INVITATION_MSG));
+				}
+				messages = Collections.singletonList(msg);
+				logLocalAction(currentState, localState, msg);
+
+				// remember that we offered to share this forum
+				localState.put(TASK, TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US);
+			}
+			else if (action == LOCAL_LEAVE) {
+				BdfDictionary msg = new BdfDictionary();
+				msg.put(TYPE, SHARE_MSG_TYPE_LEAVE);
+				msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+				msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+				messages = Collections.singletonList(msg);
+				logLocalAction(currentState, localState, msg);
+			}
+			else {
+				throw new IllegalArgumentException("Unknown Local Action");
+			}
+			return new StateUpdate<BdfDictionary, BdfDictionary>(false,
+					false, localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
+			BdfDictionary localState, BdfDictionary msg) {
+
+		try {
+			SharerProtocolState currentState =
+					getState(localState.getLong(STATE));
+			long type = msg.getLong(TYPE);
+			SharerAction action = SharerAction.getRemote(type);
+			SharerProtocolState nextState = currentState.next(action);
+			localState.put(STATE, nextState.getValue());
+
+			logMessageReceived(currentState, nextState, type, msg);
+
+			if (nextState == ERROR) {
+				if (currentState != ERROR) {
+					return abortSession(currentState, localState);
+				} else {
+					return noUpdate(localState, true);
+				}
+			}
+			List<BdfDictionary> messages = Collections.emptyList();
+			List<Event> events = Collections.emptyList();
+			boolean deleteMsg = false;
+
+			if (currentState == LEFT) {
+				// ignore and delete messages coming in while in that state
+				deleteMsg = true;
+			}
+			else if (action == REMOTE_LEAVE) {
+				localState.put(TASK, TASK_UNSHARE_FORUM_SHARED_BY_US);
+			}
+			else if (currentState == FINISHED) {
+				// ignore and delete messages coming in while in that state
+				// note that LEAVE is possible, but was handled above
+				deleteMsg = true;
+			}
+			// we have sent our invitation and just got a response
+			else if (action == REMOTE_ACCEPT || action == REMOTE_DECLINE) {
+				if (action == REMOTE_ACCEPT) {
+					localState.put(TASK, TASK_SHARE_FORUM);
+				} else {
+					// this ensures that the forum can be shared again
+					localState.put(TASK,
+							TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US);
+				}
+				String name = localState.getString(FORUM_NAME);
+				ContactId c = new ContactId(
+						localState.getLong(CONTACT_ID).intValue());
+				Event event = new ForumInvitationResponseReceivedEvent(name, c);
+				events = Collections.singletonList(event);
+			}
+			else {
+				throw new IllegalArgumentException("Bad state");
+			}
+			return new StateUpdate<BdfDictionary, BdfDictionary>(deleteMsg,
+					false, localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	private void logLocalAction(SharerProtocolState state,
+			BdfDictionary localState, BdfDictionary msg) {
+
+		if (!LOG.isLoggable(INFO)) return;
+
+		String a = "invitation";
+		if (msg.getLong(TYPE, -1L) == SHARE_MSG_TYPE_LEAVE) a = "leave";
+
+		try {
+			LOG.info("Sending " + a + " in state " + state.name() +
+					" with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " +
+					getState(localState.getLong(STATE)).name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private void logMessageReceived(SharerProtocolState currentState,
+			SharerProtocolState nextState, long type, BdfDictionary msg) {
+		if (!LOG.isLoggable(INFO)) return;
+
+		try {
+			String t = "unknown";
+			if (type == SHARE_MSG_TYPE_ACCEPT) t = "ACCEPT";
+			else if (type == SHARE_MSG_TYPE_DECLINE) t = "DECLINE";
+			else if (type == SHARE_MSG_TYPE_LEAVE) t = "LEAVE";
+			else if (type == SHARE_MSG_TYPE_ABORT) t = "ABORT";
+
+			LOG.info("Received " + t + " in state " + currentState.name() +
+					" with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " + nextState.name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
+			BdfDictionary localState, BdfDictionary delivered) {
+		try {
+			return noUpdate(localState, false);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return null;
+		}
+	}
+
+	private SharerProtocolState getState(Long state) {
+		 return SharerProtocolState.fromValue(state.intValue());
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
+			SharerProtocolState currentState, BdfDictionary localState)
+			throws FormatException {
+
+		if (LOG.isLoggable(WARNING)) {
+			LOG.warning("Aborting protocol session " +
+					Arrays.hashCode(localState.getRaw(SESSION_ID)) +
+					" in state " + currentState.name());
+		}
+
+		localState.put(STATE, ERROR.getValue());
+		BdfDictionary msg = new BdfDictionary();
+		msg.put(TYPE, SHARE_MSG_TYPE_ABORT);
+		msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+		msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+		List<BdfDictionary> messages = Collections.singletonList(msg);
+
+		List<Event> events = Collections.emptyList();
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, messages, events);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
+			BdfDictionary localState, boolean delete) throws FormatException {
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(delete, false,
+				localState, Collections.<BdfDictionary>emptyList(),
+				Collections.<Event>emptyList());
+	}
+}
diff --git a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java
similarity index 77%
rename from briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java
rename to briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java
index e10623d584a3e65ed4e662d17b43b4ac672569d2..0b87d04c19c36a768b5fcf4222b1445d5cffdaf1 100644
--- a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java
+++ b/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java
@@ -5,7 +5,7 @@ import org.junit.Test;
 
 import static org.junit.Assert.fail;
 
-public class ForumListValidatorTest extends BriarTestCase {
+public class ForumSharingValidatorTest extends BriarTestCase {
 
 	@Test
 	public void testUnitTestsExist() {