diff --git a/ROADMAP.md b/ROADMAP.md
index bed7333963665c2c825a3ae183e5d8c3a425f8bc..be7a7022c246986cb827ff29acd0fba2b193c4a0 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -54,7 +54,10 @@
     * [ ] Title
     * [ ] Avatar
   * [x] Initial chat metadata
-  * [x] User metadata (displayname/avatar)
+  * [ ] User metadata
+    * [x] Name
+    * [ ] Per-chat nickname
+    * [x] Avatar
 * Misc
   * [x] Multi-user support
   * [x] Shared group chat portals
diff --git a/alembic/versions/3c5af010538a_add_fb_receiver_to_message.py b/alembic/versions/3c5af010538a_add_fb_receiver_to_message.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0adec98111e8e69b547fbfc5470b6e78535d4da
--- /dev/null
+++ b/alembic/versions/3c5af010538a_add_fb_receiver_to_message.py
@@ -0,0 +1,38 @@
+"""Add fb_receiver to Message
+
+Revision ID: 3c5af010538a
+Revises: c36b294b1f5f
+Create Date: 2019-05-01 19:51:32.891102
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = '3c5af010538a'
+down_revision = 'c36b294b1f5f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.drop_table('message')
+    op.create_table('message',
+                    sa.Column('mxid', sa.String(length=255), nullable=True),
+                    sa.Column('mx_room', sa.String(length=255), nullable=True),
+                    sa.Column('fbid', sa.String(length=127), nullable=False),
+                    sa.Column('fb_receiver', sa.String(length=127), nullable=False),
+                    sa.Column('index', sa.SmallInteger(), nullable=False),
+                    sa.PrimaryKeyConstraint('fbid', 'fb_receiver', 'index'),
+                    sa.UniqueConstraint('mxid', 'mx_room', name='_mx_id_room'))
+
+
+def downgrade():
+    op.drop_table('message')
+    op.create_table('message',
+                    sa.Column('mxid', sa.String(length=255), nullable=True),
+                    sa.Column('mx_room', sa.String(length=255), nullable=True),
+                    sa.Column('fbid', sa.String(length=127), nullable=False),
+                    sa.Column('index', sa.SmallInteger(), nullable=False),
+                    sa.PrimaryKeyConstraint('fbid', 'index'),
+                    sa.UniqueConstraint('mxid', 'mx_room', name='_mx_id_room'))
diff --git a/mautrix_facebook/db/message.py b/mautrix_facebook/db/message.py
index e66b75ac9aba565461f8dc27bb03007bdc082bf6..e36ce0922e2306fedf2c1873dff9eb4fa2e81156 100644
--- a/mautrix_facebook/db/message.py
+++ b/mautrix_facebook/db/message.py
@@ -29,56 +29,47 @@ class Message(Base):
     mxid: EventID = Column(String(255))
     mx_room: RoomID = Column(String(255))
     fbid: str = Column(String(127), primary_key=True)
+    fb_receiver: str = Column(String(127), primary_key=True)
     index: int = Column(SmallInteger, primary_key=True, default=0)
 
     __table_args__ = (UniqueConstraint("mxid", "mx_room", name="_mx_id_room"),)
 
     @classmethod
     def scan(cls, row: RowProxy) -> 'Message':
-        mxid, mx_room, fbid, index = row
-        return cls(mxid=mxid, mx_room=mx_room, fbid=fbid, index=index)
+        mxid, mx_room, fbid, fb_receiver, index = row
+        return cls(mxid=mxid, mx_room=mx_room, fbid=fbid, fb_receiver=fb_receiver, index=index)
 
     @classmethod
-    def get_all_by_fbid(cls, fbid: str) -> Iterable['Message']:
-        return cls._select_all(cls.c.fbid == fbid)
+    def get_all_by_fbid(cls, fbid: str, fb_receiver: str) -> Iterable['Message']:
+        return cls._select_all(cls.c.fbid == fbid, cls.c.fb_receiver == fb_receiver)
 
     @classmethod
-    def get_by_fbid(cls, fbid: str, index: int) -> Optional['Message']:
-        return cls._select_one_or_none(and_(cls.c.fbid == fbid, cls.c.index == index))
+    def get_by_fbid(cls, fbid: str, fb_receiver: str, index: int) -> Optional['Message']:
+        return cls._select_one_or_none(and_(cls.c.fbid == fbid, cls.c.fb_receiver == fb_receiver,
+                                            cls.c.index == index))
 
     @classmethod
     def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional['Message']:
         return cls._select_one_or_none(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room))
 
-    @classmethod
-    def update_by_fbid(cls, s_fbid: str, s_index: int,
-                       **values) -> None:
-        with cls.db.begin() as conn:
-            conn.execute(cls.t.update()
-                         .where(and_(cls.c.fbid == s_fbid, cls.c.index == s_index))
-                         .values(**values))
-
-    @classmethod
-    def update_by_mxid(cls, s_mxid: EventID, s_mx_room: RoomID, **values) -> None:
-        with cls.db.begin() as conn:
-            conn.execute(cls.t.update()
-                         .where(and_(cls.c.mxid == s_mxid, cls.c.mx_room == s_mx_room))
-                         .values(**values))
-
     @property
     def _edit_identity(self):
-        return and_(self.c.fbid == self.fbid, self.c.index == self.index)
+        return and_(self.c.fbid == self.fbid, self.c.fb_receiver == self.fb_receiver,
+                    self.c.index == self.index)
 
     @classmethod
-    def bulk_create(cls, fbid: str, event_ids: List[EventID], mx_room: RoomID) -> None:
+    def bulk_create(cls, fbid: str, fb_receiver: str, event_ids: List[EventID], mx_room: RoomID
+                    ) -> None:
         if not event_ids:
             return
         with cls.db.begin() as conn:
             conn.execute(cls.t.insert(),
-                         [dict(mxid=event_id, mx_room=mx_room, fbid=fbid, index=i)
+                         [dict(mxid=event_id, mx_room=mx_room, fbid=fbid, fb_receiver=fb_receiver,
+                               index=i)
                           for i, event_id in enumerate(event_ids)])
 
     def insert(self) -> None:
         with self.db.begin() as conn:
             conn.execute(self.t.insert().values(mxid=self.mxid, mx_room=self.mx_room,
-                                                fbid=self.fbid, index=self.index))
+                                                fb_receiver=self.fb_receiver, fbid=self.fbid,
+                                                index=self.index))
diff --git a/mautrix_facebook/portal.py b/mautrix_facebook/portal.py
index c1b03d5d0e97de5c7b3221677d4033a6cac5816f..1bc7a663d40392b30a4664a93543c3cd945f952e 100644
--- a/mautrix_facebook/portal.py
+++ b/mautrix_facebook/portal.py
@@ -15,6 +15,7 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 from typing import Dict, Deque, Optional, Tuple, Union, TYPE_CHECKING
 from collections import deque
+from contextlib import asynccontextmanager
 import asyncio
 import logging
 
@@ -69,6 +70,7 @@ class Portal:
     _last_bridged_mxid: EventID
     _dedup: Deque[Tuple[str, str]]
     _avatar_uri: Optional[ContentURI]
+    _send_locks: Dict[str, asyncio.Lock]
 
     def __init__(self, fbid: str, fb_receiver: str, fb_type: ThreadType,
                  mxid: Optional[RoomID] = None, name: str = "", photo_id: str = "",
@@ -87,6 +89,7 @@ class Portal:
         self._create_room_lock = asyncio.Lock()
         self._dedup = deque(maxlen=100)
         self._avatar_uri = None
+        self._send_locks = {}
 
         self.log = self.log.getChild(self.fbid_log)
 
@@ -249,21 +252,47 @@ class Portal:
     # endregion
     # region Matrix event handling
 
+    @asynccontextmanager
+    async def require_send_lock(self, user_id: str) -> None:
+        try:
+            lock = self._send_locks[user_id]
+        except KeyError:
+            lock = asyncio.Lock()
+            self._send_locks[user_id] = lock
+        async with lock:
+            yield
+
+    @asynccontextmanager
+    async def optional_send_lock(self, user_id: str) -> None:
+        try:
+            lock = self._send_locks[user_id]
+        except KeyError:
+            yield
+            return
+        async with lock:
+            yield
+
     async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent,
                                     event_id: EventID) -> None:
-        if message.msgtype == MessageType.TEXT or message.msgtype == MessageType.NOTICE:
-            fbid = await self._handle_matrix_text(sender, message)
-        elif message.msgtype == MessageType.IMAGE:
-            fbid = await self._handle_matrix_image(sender, message)
-        elif message.msgtype == MessageType.LOCATION:
-            fbid = await self._handle_matrix_location(sender, message)
-        else:
-            self.log.warn(f"Unsupported msgtype in {message}")
-            return
-        if not fbid:
-            return
-        DBMessage(mxid=event_id, mx_room=self.mxid, fbid=fbid, index=0).insert()
-        self._last_bridged_mxid = event_id
+        # TODO this probably isn't nice for bridging images, it really only needs to lock the
+        #      actual message send call and dedup queue append.
+        async with self.require_send_lock(sender.uid):
+            if message.msgtype == MessageType.TEXT or message.msgtype == MessageType.NOTICE:
+                fbid = await self._handle_matrix_text(sender, message)
+            elif message.msgtype == MessageType.IMAGE:
+                fbid = await self._handle_matrix_image(sender, message)
+            elif message.msgtype == MessageType.LOCATION:
+                fbid = await self._handle_matrix_location(sender, message)
+            else:
+                self.log.warn(f"Unsupported msgtype in {message}")
+                return
+            if not fbid:
+                return
+            self._dedup.appendleft(fbid)
+            DBMessage(mxid=event_id, mx_room=self.mxid,
+                      fbid=fbid, fb_receiver=self.fb_receiver,
+                      index=0).insert()
+            self._last_bridged_mxid = event_id
 
     async def _handle_matrix_text(self, sender: 'u.User', message: TextMessageEventContent) -> str:
         return await sender.send(FBMessage(text=message.body), self.fbid, self.fb_type)
@@ -296,10 +325,11 @@ class Portal:
 
     async def handle_facebook_message(self, source: 'u.User', sender: 'p.Puppet',
                                       message: FBMessage) -> None:
-        if message.uid in self._dedup:
-            await source.markAsDelivered(self.fbid, message.uid)
-            return
-        self._dedup.appendleft(message.uid)
+        async with self.optional_send_lock(sender.fbid):
+            if message.uid in self._dedup:
+                await source.markAsDelivered(self.fbid, message.uid)
+                return
+            self._dedup.appendleft(message.uid)
         if not self.mxid:
             await self.create_matrix_room(source)
         if message.sticker:
@@ -313,7 +343,7 @@ class Portal:
         else:
             self.log.debug(f"Unhandled Messenger message: {message}")
             event_ids = []
-        DBMessage.bulk_create(fbid=message.uid, mx_room=self.mxid,
+        DBMessage.bulk_create(fbid=message.uid, fb_receiver=self.fb_receiver, mx_room=self.mxid,
                               event_ids=[event_id for event_id in event_ids if event_id])
         await source.markAsDelivered(self.fbid, message.uid)
 
@@ -331,7 +361,7 @@ class Portal:
                                          text=sticker.label)
 
     async def _handle_facebook_attachment(self, intent: IntentAPI, attachment: AttachmentClass
-                                          ) -> EventID:
+                                          ) -> Optional[EventID]:
         if isinstance(attachment, AudioAttachment):
             mxc, mime, size = await self._reupload_fb_photo(attachment.url, intent,
                                                             attachment.filename)
@@ -390,7 +420,7 @@ class Portal:
                                      ) -> None:
         if not self.mxid:
             return
-        for message in DBMessage.get_all_by_fbid(message_id):
+        for message in DBMessage.get_all_by_fbid(message_id, self.fb_receiver):
             try:
                 await sender.intent.redact(message.mx_room, message.mxid)
             except MForbidden:
diff --git a/mautrix_facebook/user.py b/mautrix_facebook/user.py
index 18981b519743198a255f4f1e3e86872097458237..a134b5a0b1ea046f980c61de3ae18e8d85c59aea 100644
--- a/mautrix_facebook/user.py
+++ b/mautrix_facebook/user.py
@@ -49,7 +49,7 @@ class User(Client):
     is_whitelisted: bool
     is_admin: bool
     _is_logged_in: Optional[bool]
-    _session_data: SimpleCookie
+    _session_data: Optional[SimpleCookie]
     _db_instance: Optional[DBUser]
 
     def __init__(self, mxid: UserID, session: Optional[SimpleCookie] = None,
@@ -78,9 +78,10 @@ class User(Client):
             self._db_instance = DBUser(mxid=self.mxid)
         return self._db_instance
 
-    def save(self) -> None:
+    def save(self, _update_session_data: bool = True) -> None:
         self.log.debug("Saving session")
-        self._session_data = self.getSession()
+        if _update_session_data:
+            self._session_data = self.getSession()
         self.db_instance.edit(session=self._session_data, fbid=self.uid)
 
     @classmethod
@@ -131,6 +132,12 @@ class User(Client):
 
     # endregion
 
+    async def logout(self) -> bool:
+        ok = await super().logout()
+        self._session_data = None
+        self.save(_update_session_data=False)
+        return ok
+
     async def post_login(self) -> None:
         self.log.info("Running post-login actions")
         await self.sync_threads()
diff --git a/requirements.txt b/requirements.txt
index aa8f1ab89efdee5b30881c2cd43b03a7b041b1de..f377a99b9ecfdd083d4c92fd6b58670f1e820bc9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,6 @@ SQLAlchemy
 alembic
 ruamel.yaml
 commonmark
+python-magic
 git+https://github.com/tulir/fbchat-asyncio@master#egg=fbchat-asyncio
 git+https://github.com/tulir/mautrix-python@matrix-restructure#egg=mautrix
diff --git a/setup.py b/setup.py
index 1b6b466e1288f22743bb34ad427dd7983dd3c652..77bd21e0d93e67bccf5aade7f1d03bc2bab83ee0 100644
--- a/setup.py
+++ b/setup.py
@@ -25,6 +25,7 @@ setuptools.setup(
         "mautrix>=0.4.0.dev31,<0.5.0",
         "ruamel.yaml>=0.15.94,<0.16",
         "commonmark>=0.8,<0.9",
+        "python-magic>=0.4,<0.5",
         "fbchat-asyncio>=0.1.1,<0.2.0",
         "SQLAlchemy>=1.2,<2",
         "alembic>=1,<2",