diff --git a/gradle.properties b/gradle.properties
index 28c5fc77c22cb5dd8b01c89c90a2ed08e1eca25a..5b97f2a777eb147bd02e23a888d485ca4d961cbb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,7 @@
 org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m
 kotlin.code.style=official
+
+bouncyCastleVersion=1.68
 hamcrestVersion=2.1
 junit5Version=5.6.0
 kotlinBitflagsVersion=1.1.0
diff --git a/libquassel-protocol/build.gradle.kts b/libquassel-protocol/build.gradle.kts
index 4ce962d86f96cfe2b698d4e1c3748890460b1a9e..530c483a520e1e252f534525b261fbf4480109d6 100644
--- a/libquassel-protocol/build.gradle.kts
+++ b/libquassel-protocol/build.gradle.kts
@@ -17,6 +17,8 @@ dependencies {
   api("org.threeten", "threetenbp", "1.4.0")
   val kotlinBitflagsVersion: String by project
   api("de.justjanne", "kotlin-bitflags", kotlinBitflagsVersion)
+  val bouncyCastleVersion: String by project
+  implementation("org.bouncycastle", "bcpkix-jdk15on", bouncyCastleVersion)
   api(project(":libquassel-annotations"))
   ksp(project(":libquassel-generator"))
 }
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/ConnectedClient.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/ConnectedClient.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d4eae08744666ccaa463dc5641e37426f299ec26
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/ConnectedClient.kt
@@ -0,0 +1,66 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.models
+
+import de.justjanne.bitflags.of
+import de.justjanne.bitflags.toBits
+import de.justjanne.libquassel.protocol.features.FeatureSet
+import de.justjanne.libquassel.protocol.features.LegacyFeature
+import de.justjanne.libquassel.protocol.features.QuasselFeatureName
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.variant.QVariantMap
+import de.justjanne.libquassel.protocol.variant.into
+import de.justjanne.libquassel.protocol.variant.qVariant
+import org.threeten.bp.Instant
+
+data class ConnectedClient(
+  val id: Int,
+  val remoteAddress: String,
+  val location: String,
+  val version: String,
+  val versionDate: Instant?,
+  val connectedSince: Instant,
+  val secure: Boolean,
+  val features: FeatureSet
+) {
+  fun toVariantMap() = mapOf(
+    "id" to qVariant(id, QtType.Int),
+    "remoteAddress" to qVariant(remoteAddress, QtType.QString),
+    "location" to qVariant(location, QtType.QString),
+    "clientVersion" to qVariant(version, QtType.QString),
+    "clientVersionDate" to qVariant(versionDate?.epochSecond?.toString(), QtType.QString),
+    "connectedSince" to qVariant(connectedSince, QtType.QDateTime),
+    "secure" to qVariant(secure, QtType.Bool),
+    "features" to qVariant(features.legacyFeatures().toBits(), QtType.UInt),
+    "featureList" to qVariant(features.featureList().map(QuasselFeatureName::name), QtType.QStringList)
+  )
+
+  companion object {
+    fun fromVariantMap(properties: QVariantMap) = ConnectedClient(
+      id = properties["id"].into(-1),
+      remoteAddress = properties["remoteAddress"].into(""),
+      location = properties["location"].into(""),
+      version = properties["clientVersion"].into(""),
+      versionDate = properties["clientVersionDate"].into("")
+        .toLongOrNull()
+        ?.let(Instant::ofEpochSecond),
+      connectedSince = properties["connectedSince"].into(Instant.EPOCH),
+      secure = properties["secure"].into(false),
+      features = FeatureSet.build(
+        LegacyFeature.of(properties["features"].into()),
+        properties["featureList"].into<QStringList>()
+          ?.filterNotNull()
+          ?.map(::QuasselFeatureName)
+          .orEmpty()
+      )
+    )
+  }
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt
index 4d6d996c1f21e7c40687f02902f1135bb0657186..8cf451b25fb7c677d09b5a2f0361033894a3ed58 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightNickType.kt
@@ -11,11 +11,11 @@
 package de.justjanne.libquassel.protocol.models
 
 enum class HighlightNickType(
-  val value: UByte,
+  val value: Int,
 ) {
-  NoNick(0x00u),
-  CurrentNick(0x01u),
-  AllNicks(0x02u);
+  NoNick(0),
+  CurrentNick(1),
+  AllNicks(2);
 
   companion object {
     private val values = enumValues<HighlightNickType>()
@@ -24,6 +24,6 @@ enum class HighlightNickType(
     /**
      * Obtain from underlying representation
      */
-    fun of(value: UByte): HighlightNickType? = values[value]
+    fun of(value: Int): HighlightNickType? = values[value]
   }
 }
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightRule.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightRule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..af45a7162c077b8902bba5f4dfe8710e80cf55df
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/models/HighlightRule.kt
@@ -0,0 +1,43 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.models
+
+import de.justjanne.libquassel.protocol.util.expression.ExpressionMatch
+
+data class HighlightRule(
+  val id: Int,
+  val contents: String,
+  val isRegEx: Boolean = false,
+  val isCaseSensitive: Boolean = false,
+  val isEnabled: Boolean = true,
+  val isInverse: Boolean = false,
+  val sender: String,
+  val channel: String
+) {
+  val contentMatch = ExpressionMatch(
+    contents,
+    if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx
+    else ExpressionMatch.MatchMode.MatchPhrase,
+    isCaseSensitive
+  )
+  val senderMatch = ExpressionMatch(
+    sender,
+    if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx
+    else ExpressionMatch.MatchMode.MatchMultiWildcard,
+    isCaseSensitive
+  )
+  val channelMatch = ExpressionMatch(
+    channel,
+    if (isRegEx) ExpressionMatch.MatchMode.MatchRegEx
+    else ExpressionMatch.MatchMode.MatchMultiWildcard,
+    isCaseSensitive
+  )
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt
index d392cb32528ab099f2cfbfa4f0865fb16f13f337..95fd8c0edc8d4590ae6e38285fc4a1d17302c110 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/AliasManager.kt
@@ -22,28 +22,25 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap
 import de.justjanne.libquassel.protocol.variant.into
 import de.justjanne.libquassel.protocol.variant.qVariant
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 
 class AliasManager constructor(
   session: Session
 ) : SyncableObject(session, "AliasManager"), AliasManagerStub {
   override fun toVariantMap(): QVariantMap = mapOf(
-    "Aliases" to qVariant(initAliases(), QtType.QVariantMap)
+    "Aliases" to qVariant(
+      mapOf(
+        "names" to qVariant(aliases().map(Alias::name), QtType.QStringList),
+        "expansions" to qVariant(aliases().map(Alias::expansion), QtType.QStringList)
+      ),
+      QtType.QVariantMap
+    )
   )
 
   override fun fromVariantMap(properties: QVariantMap) {
-    initSetAliases(properties["Aliases"].into<QVariantMap>().orEmpty())
-  }
-
-  private fun initAliases(): QVariantMap = mapOf(
-    "names" to qVariant(aliases().map(Alias::name), QtType.QStringList),
-    "expansions" to qVariant(aliases().map(Alias::expansion), QtType.QStringList)
-  )
+    val aliases = properties["Aliases"].into<QVariantMap>().orEmpty()
 
-  private fun initSetAliases(aliases: QVariantMap) {
     val names = aliases["names"].into<QStringList>().orEmpty()
     val expansions = aliases["expansions"].into<List<String>>().orEmpty()
-
     require(names.size == expansions.size) {
       "Sizes do not match: names=${names.size}, expansions=${expansions.size}"
     }
@@ -107,7 +104,7 @@ class AliasManager constructor(
   inline fun state() = flow().value
 
   @Suppress("NOTHING_TO_INLINE")
-  inline fun flow(): StateFlow<AliasManagerState> = state
+  inline fun flow() = state
 
   @PublishedApi
   internal val state = MutableStateFlow(
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BacklogManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BacklogManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BacklogManager.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferSyncer.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferSyncer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferSyncer.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfig.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewConfig.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/BufferViewManager.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CertManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CertManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f2d51725d618a804b628d6eebb445fc1056779c6
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CertManager.kt
@@ -0,0 +1,126 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
+
+import de.justjanne.libquassel.protocol.models.ids.IdentityId
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.serializers.qt.StringSerializerUtf8
+import de.justjanne.libquassel.protocol.syncables.state.CertManagerState
+import de.justjanne.libquassel.protocol.syncables.stubs.CertManagerStub
+import de.justjanne.libquassel.protocol.util.update
+import de.justjanne.libquassel.protocol.variant.QVariantMap
+import de.justjanne.libquassel.protocol.variant.into
+import de.justjanne.libquassel.protocol.variant.qVariant
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.bouncycastle.cert.X509CertificateHolder
+import org.bouncycastle.openssl.PEMKeyPair
+import org.bouncycastle.openssl.PEMParser
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
+import java.nio.ByteBuffer
+import java.security.PrivateKey
+import java.security.cert.Certificate
+import java.security.cert.CertificateFactory
+
+class CertManager(
+  identityId: IdentityId,
+  session: Session
+) : SyncableObject(session, "CertManager"), CertManagerStub {
+  override fun fromVariantMap(properties: QVariantMap) {
+    val privateKeyPem = properties["sslKey"].into("")
+    val certPem = properties["sslCert"].into("")
+
+    state.update {
+      copy(
+        privateKeyPem = privateKeyPem,
+        certificatePem = certPem,
+        privateKey = readPrivateKey(privateKeyPem),
+        certificate = readCertificate(certPem)
+      )
+    }
+  }
+
+  override fun toVariantMap() = mapOf(
+    "sslKey" to qVariant(StringSerializerUtf8.serializeRaw(state().certificatePem), QtType.QByteArray),
+    "sslCert" to qVariant(StringSerializerUtf8.serializeRaw(state().privateKeyPem), QtType.QByteArray)
+  )
+
+  private fun readPrivateKey(pem: String): PrivateKey? {
+    if (pem.isBlank()) {
+      return null
+    }
+
+    try {
+      val keyPair = PEMParser(pem.reader()).readObject() as? PEMKeyPair
+        ?: return null
+      return JcaPEMKeyConverter().getPrivateKey(keyPair.privateKeyInfo)
+    } catch (t: Throwable) {
+      return null
+    }
+  }
+
+  private fun readCertificate(pem: String): Certificate? {
+    if (pem.isBlank()) {
+      return null
+    }
+
+    try {
+      val certificate = PEMParser(pem.reader()).readObject() as? X509CertificateHolder
+        ?: return null
+      return CertificateFactory.getInstance("X.509")
+        .generateCertificate(certificate.encoded.inputStream())
+    } catch (t: Throwable) {
+      return null
+    }
+  }
+
+  override fun setSslKey(encoded: ByteBuffer) {
+    val pem = StringSerializerUtf8.deserializeRaw(encoded)
+
+    state.update {
+      copy(
+        privateKeyPem = pem,
+        privateKey = readPrivateKey(pem)
+      )
+    }
+    super.setSslKey(encoded)
+  }
+
+  override fun setSslCert(encoded: ByteBuffer) {
+    val pem = StringSerializerUtf8.deserializeRaw(encoded)
+
+    state.update {
+      copy(
+        certificatePem = pem,
+        certificate = readCertificate(pem)
+      )
+    }
+    super.setSslCert(encoded)
+  }
+
+  fun privateKey() = state().privateKey
+  fun privateKeyPem() = state().privateKeyPem
+
+  fun certificate() = state().certificate
+  fun certificatePem() = state().certificatePem
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun state() = flow().value
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun flow() = state
+
+  @PublishedApi
+  internal val state = MutableStateFlow(
+    CertManagerState(
+      identityId = identityId
+    )
+  )
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CoreInfo.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CoreInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95933ef3384e57b07a8958d014efa7475569de63
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/CoreInfo.kt
@@ -0,0 +1,82 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
+
+import de.justjanne.libquassel.protocol.models.ConnectedClient
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.syncables.state.CoreInfoState
+import de.justjanne.libquassel.protocol.syncables.stubs.CoreInfoStub
+import de.justjanne.libquassel.protocol.util.update
+import de.justjanne.libquassel.protocol.variant.QVariantList
+import de.justjanne.libquassel.protocol.variant.QVariantMap
+import de.justjanne.libquassel.protocol.variant.QVariant_
+import de.justjanne.libquassel.protocol.variant.into
+import de.justjanne.libquassel.protocol.variant.qVariant
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.threeten.bp.Instant
+
+class CoreInfo constructor(
+  session: Session
+) : SyncableObject(session, "CoreInfo"), CoreInfoStub {
+  override fun fromVariantMap(properties: QVariantMap) {
+    val coreData = properties["coreData"].into<QVariantMap>().orEmpty()
+
+    state.update {
+      copy(
+        version = coreData["quasselVersion"].into(version),
+        versionDate = coreData["quasselBuildDate"].into("")
+          .toLongOrNull()
+          ?.let(Instant::ofEpochSecond),
+        startTime = coreData["startTime"].into(startTime),
+        connectedClientCount = coreData["sessionConnectedClients"].into(connectedClientCount),
+        connectedClients = coreData["sessionConnectedClientData"].into<QVariantList>()
+          ?.mapNotNull<QVariant_, QVariantMap>(QVariant_::into)
+          ?.map(ConnectedClient.Companion::fromVariantMap)
+          .orEmpty()
+      )
+    }
+  }
+
+  override fun toVariantMap() = mapOf(
+    "quasselVersion" to qVariant(version(), QtType.QString),
+    "quasselBuildDate" to qVariant(versionDate()?.epochSecond?.toString(), QtType.QString),
+    "startTime" to qVariant(startTime(), QtType.QDateTime),
+    "sessionConnectedClients" to qVariant(connectedClientCount(), QtType.Int),
+    "sessionConnectedClientData" to qVariant(
+      connectedClients()
+        .map(ConnectedClient::toVariantMap)
+        .map { qVariant(it, QtType.QVariantMap) },
+      QtType.QVariantList
+    )
+  )
+
+  fun version() = state().version
+  fun versionDate() = state().versionDate
+  fun startTime() = state().startTime
+  fun connectedClientCount() = state().connectedClientCount
+  fun connectedClients() = state().connectedClients
+
+  override fun setCoreData(data: QVariantMap) {
+    fromVariantMap(data)
+    super.setCoreData(data)
+  }
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun state() = flow().value
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun flow() = state
+
+  @PublishedApi
+  internal val state = MutableStateFlow(
+    CoreInfoState()
+  )
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/DccConfig.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/DccConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..991995bc6dad37bf036b3e47bc139e4982e22a28
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/DccConfig.kt
@@ -0,0 +1,154 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
+
+import de.justjanne.libquassel.protocol.models.DccIpDetectionMode
+import de.justjanne.libquassel.protocol.models.DccPortSelectionMode
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.models.types.QuasselType
+import de.justjanne.libquassel.protocol.syncables.state.DccConfigState
+import de.justjanne.libquassel.protocol.syncables.stubs.DccConfigStub
+import de.justjanne.libquassel.protocol.util.update
+import de.justjanne.libquassel.protocol.variant.QVariantMap
+import de.justjanne.libquassel.protocol.variant.into
+import de.justjanne.libquassel.protocol.variant.qVariant
+import kotlinx.coroutines.flow.MutableStateFlow
+import java.net.InetAddress
+
+class DccConfig constructor(
+  session: Session
+) : SyncableObject(session, "DccConfig"), DccConfigStub {
+  override fun init() {
+    renameObject("DccConfig")
+  }
+
+  override fun fromVariantMap(properties: QVariantMap) {
+    state.update {
+      copy(
+        dccEnabled = properties["dccEnabled"].into(dccEnabled),
+        outgoingIp = properties["outgoingIp"].into(outgoingIp),
+        ipDetectionMode = properties["ipDetectionMode"].into(ipDetectionMode),
+        portSelectionMode = properties["portSelectionMode"].into(portSelectionMode),
+        minPort = properties["minPort"].into(minPort),
+        maxPort = properties["maxPort"].into(maxPort),
+        chunkSize = properties["chunkSize"].into(chunkSize),
+        sendTimeout = properties["sendTimeout"].into(sendTimeout),
+        usePassiveDcc = properties["usePassiveDcc"].into(usePassiveDcc),
+        useFastSend = properties["useFastSend"].into(useFastSend),
+      )
+    }
+  }
+
+  override fun toVariantMap() = mapOf(
+    "dccEnabled" to qVariant(state().dccEnabled, QtType.Bool),
+    "outgoingIp" to qVariant(state().outgoingIp, QuasselType.QHostAddress),
+    "ipDetectionMode" to qVariant(state().ipDetectionMode, QuasselType.DccConfigIpDetectionMode),
+    "portSelectionMode" to qVariant(state().portSelectionMode, QuasselType.DccConfigPortSelectionMode),
+    "minPort" to qVariant(state().minPort, QtType.UShort),
+    "maxPort" to qVariant(state().maxPort, QtType.UShort),
+    "chunkSize" to qVariant(state().chunkSize, QtType.Int),
+    "sendTimeout" to qVariant(state().sendTimeout, QtType.Int),
+    "usePassiveDcc" to qVariant(state().usePassiveDcc, QtType.Bool),
+    "useFastSend" to qVariant(state().useFastSend, QtType.Bool)
+  )
+
+  override fun setDccEnabled(enabled: Boolean) {
+    state.update {
+      copy(dccEnabled = enabled)
+    }
+    super.setDccEnabled(enabled)
+  }
+
+  override fun setOutgoingIp(outgoingIp: InetAddress) {
+    state.update {
+      copy(outgoingIp = outgoingIp)
+    }
+    super.setOutgoingIp(outgoingIp)
+  }
+
+  override fun setIpDetectionMode(ipDetectionMode: DccIpDetectionMode) {
+    state.update {
+      copy(ipDetectionMode = ipDetectionMode)
+    }
+    super.setIpDetectionMode(ipDetectionMode)
+  }
+
+  override fun setPortSelectionMode(portSelectionMode: DccPortSelectionMode) {
+    state.update {
+      copy(portSelectionMode = portSelectionMode)
+    }
+    super.setPortSelectionMode(portSelectionMode)
+  }
+
+  override fun setMinPort(port: UShort) {
+    state.update {
+      copy(minPort = port)
+    }
+    super.setMinPort(port)
+  }
+
+  override fun setMaxPort(port: UShort) {
+    state.update {
+      copy(maxPort = port)
+    }
+    super.setMaxPort(port)
+  }
+
+  override fun setChunkSize(chunkSize: Int) {
+    state.update {
+      copy(chunkSize = chunkSize)
+    }
+    super.setChunkSize(chunkSize)
+  }
+
+  override fun setSendTimeout(timeout: Int) {
+    state.update {
+      copy(sendTimeout = timeout)
+    }
+    super.setSendTimeout(timeout)
+  }
+
+  override fun setUsePassiveDcc(use: Boolean) {
+    state.update {
+      copy(usePassiveDcc = use)
+    }
+    super.setUsePassiveDcc(use)
+  }
+
+  override fun setUseFastSend(use: Boolean) {
+    state.update {
+      copy(useFastSend = use)
+    }
+    super.setUseFastSend(use)
+  }
+
+  fun isDccEnabled() = state().dccEnabled
+  fun outgoingIp() = state().outgoingIp
+  fun ipDetectionMode() = state().ipDetectionMode
+  fun portSelectionMode() = state().portSelectionMode
+  fun minPort() = state().minPort
+  fun maxPort() = state().maxPort
+  fun chunkSize() = state().chunkSize
+  fun sendTimeout() = state().sendTimeout
+  fun usePassiveDcc() = state().usePassiveDcc
+  fun useFastSend() = state().useFastSend
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun state() = flow().value
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun flow() = state
+
+  @PublishedApi
+  internal val state = MutableStateFlow(
+    DccConfigState()
+  )
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..20dd19ca5330faf6bbd25732dcf56d8f4a438e72
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/HighlightRuleManager.kt
@@ -0,0 +1,214 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
+
+import de.justjanne.libquassel.protocol.models.HighlightNickType
+import de.justjanne.libquassel.protocol.models.HighlightRule
+import de.justjanne.libquassel.protocol.models.QStringList
+import de.justjanne.libquassel.protocol.models.types.QtType
+import de.justjanne.libquassel.protocol.syncables.state.HighlightRuleManagerState
+import de.justjanne.libquassel.protocol.syncables.stubs.HighlightRuleManagerStub
+import de.justjanne.libquassel.protocol.util.update
+import de.justjanne.libquassel.protocol.variant.QVariantList
+import de.justjanne.libquassel.protocol.variant.QVariantMap
+import de.justjanne.libquassel.protocol.variant.into
+import de.justjanne.libquassel.protocol.variant.qVariant
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class HighlightRuleManager(
+  session: Session
+) : SyncableObject(session, "HighlightRuleManager"), HighlightRuleManagerStub {
+  override fun fromVariantMap(properties: QVariantMap) {
+    val highlightRules = properties["HighlightRuleList"].into<QVariantMap>().orEmpty()
+
+    val idList = highlightRules["id"].into<QVariantList>().orEmpty()
+    val nameList = highlightRules["name"].into<QStringList>().orEmpty()
+    val isRegExList = highlightRules["isRegEx"].into<QVariantList>().orEmpty()
+    val isCaseSensitiveList = highlightRules["isCaseSensitive"].into<QVariantList>().orEmpty()
+    val isEnabledList = highlightRules["isEnabled"].into<QVariantList>().orEmpty()
+    val isInverseList = highlightRules["isInverse"].into<QVariantList>().orEmpty()
+    val senderList = highlightRules["sender"].into<QStringList>().orEmpty()
+    val channelList = highlightRules["channel"].into<QStringList>().orEmpty()
+
+    require(idList.size == nameList.size) {
+      "Sizes do not match: ids=${idList.size}, nameList=${nameList.size}"
+    }
+    require(idList.size == isRegExList.size) {
+      "Sizes do not match: ids=${idList.size}, isRegExList=${isRegExList.size}"
+    }
+    require(idList.size == isCaseSensitiveList.size) {
+      "Sizes do not match: ids=${idList.size}, isCaseSensitiveList=${isCaseSensitiveList.size}"
+    }
+    require(idList.size == isEnabledList.size) {
+      "Sizes do not match: ids=${idList.size}, isEnabledList=${isEnabledList.size}"
+    }
+    require(idList.size == isInverseList.size) {
+      "Sizes do not match: ids=${idList.size}, isInverseList=${isInverseList.size}"
+    }
+    require(idList.size == senderList.size) {
+      "Sizes do not match: ids=${idList.size}, senderList=${senderList.size}"
+    }
+    require(idList.size == channelList.size) {
+      "Sizes do not match: ids=${idList.size}, channelList=${channelList.size}"
+    }
+    require(idList.size == channelList.size) {
+      "Sizes do not match: ids=${idList.size}, channelList=${channelList.size}"
+    }
+
+    state.update {
+      copy(
+        highlightNickType = properties["highlightNick"].into<Int>()
+          ?.let(HighlightNickType.Companion::of)
+          ?: highlightNickType,
+        highlightNickCaseSensitive = properties["nicksCaseSensitive"].into(highlightNickCaseSensitive),
+        rules = List(idList.size) {
+          HighlightRule(
+            idList[it].into(0),
+            nameList[it] ?: "",
+            isRegEx = isRegExList[it].into(false),
+            isCaseSensitive = isCaseSensitiveList[it].into(false),
+            isEnabled = isEnabledList[it].into(false),
+            isInverse = isInverseList[it].into(false),
+            sender = senderList[it] ?: "",
+            channel = channelList[it] ?: ""
+          )
+        }
+      )
+    }
+  }
+
+  override fun toVariantMap() = mapOf(
+    "HighlightRuleList" to qVariant(
+      mapOf(
+        "id" to qVariant(
+          state().rules.map { qVariant(it.id, QtType.Int) },
+          QtType.QVariantList
+        ),
+        "name" to qVariant(
+          state().rules.map(HighlightRule::contents),
+          QtType.QStringList
+        ),
+        "isRegEx" to qVariant(
+          state().rules.map { qVariant(it.isRegEx, QtType.Bool) },
+          QtType.QVariantList
+        ),
+        "isCaseSensitive" to qVariant(
+          state().rules.map { qVariant(it.isCaseSensitive, QtType.Bool) },
+          QtType.QVariantList
+        ),
+        "isEnabled" to qVariant(
+          state().rules.map { qVariant(it.isEnabled, QtType.Bool) },
+          QtType.QVariantList
+        ),
+        "isInverse" to qVariant(
+          state().rules.map { qVariant(it.isInverse, QtType.Bool) },
+          QtType.QVariantList
+        ),
+        "sender" to qVariant(
+          state().rules.map(HighlightRule::sender),
+          QtType.QStringList
+        ),
+        "channel" to qVariant(
+          state().rules.map(HighlightRule::channel),
+          QtType.QStringList
+        ),
+      ),
+      QtType.QVariantMap
+    ),
+    "highlightNick" to qVariant(state().highlightNickType.value, QtType.Int),
+    "nicksCaseSensitive" to qVariant(state().highlightNickCaseSensitive, QtType.Bool)
+  )
+
+  fun indexOf(id: Int): Int = state().indexOf(id)
+  fun contains(id: Int) = state().contains(id)
+
+  fun isEmpty() = state().isEmpty()
+  fun count() = state().count()
+  fun removeAt(index: Int) {
+    state.update {
+      copy(rules = rules.drop(index))
+    }
+  }
+
+  override fun removeHighlightRule(highlightRule: Int) {
+    removeAt(indexOf(highlightRule))
+    super.removeHighlightRule(highlightRule)
+  }
+
+  override fun toggleHighlightRule(highlightRule: Int) {
+    state.update {
+      copy(
+        rules = rules.map {
+          if (it.id != highlightRule) it
+          else it.copy(isEnabled = !it.isEnabled)
+        }
+      )
+    }
+    super.toggleHighlightRule(highlightRule)
+  }
+
+  override fun addHighlightRule(
+    id: Int,
+    name: String?,
+    isRegEx: Boolean,
+    isCaseSensitive: Boolean,
+    isEnabled: Boolean,
+    isInverse: Boolean,
+    sender: String?,
+    chanName: String?
+  ) {
+    if (contains(id)) {
+      return
+    }
+
+    state.update {
+      copy(
+        rules = rules + HighlightRule(
+          id,
+          name ?: "",
+          isRegEx,
+          isCaseSensitive,
+          isEnabled,
+          isInverse,
+          sender ?: "",
+          chanName ?: ""
+        )
+      )
+    }
+
+    super.addHighlightRule(id, name, isRegEx, isCaseSensitive, isEnabled, isInverse, sender, chanName)
+  }
+
+  override fun setHighlightNick(highlightNick: Int) {
+    state.update {
+      copy(highlightNickType = HighlightNickType.of(highlightNick) ?: highlightNickType)
+    }
+    super.setHighlightNick(highlightNick)
+  }
+
+  override fun setNicksCaseSensitive(nicksCaseSensitive: Boolean) {
+    state.update {
+      copy(highlightNickCaseSensitive = nicksCaseSensitive)
+    }
+    super.setNicksCaseSensitive(nicksCaseSensitive)
+  }
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun state() = flow().value
+
+  @Suppress("NOTHING_TO_INLINE")
+  inline fun flow() = state
+
+  @PublishedApi
+  internal val state = MutableStateFlow(
+    HighlightRuleManagerState()
+  )
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Identity.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Identity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Identity.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt
index fbbfd8ad83e4e00ad4957749d149b94c5a8156bb..885b4f01a8490bb7bc4988d79d6177830317358c 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcChannel.kt
@@ -23,7 +23,6 @@ import de.justjanne.libquassel.protocol.variant.indexed
 import de.justjanne.libquassel.protocol.variant.into
 import de.justjanne.libquassel.protocol.variant.qVariant
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 
 class IrcChannel(
   name: String,
@@ -269,7 +268,7 @@ class IrcChannel(
   inline fun state() = flow().value
 
   @Suppress("NOTHING_TO_INLINE")
-  inline fun flow(): StateFlow<IrcChannelState> = state
+  inline fun flow() = state
 
   @PublishedApi
   internal val state = MutableStateFlow(
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcListHelper.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcListHelper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcListHelper.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt
index c492dc53be450ca32d2290d6b6c3ca7320d5e570..115f120ae5d5ad0a11d9d62d0627f6ca35234ae1 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/IrcUser.kt
@@ -21,7 +21,6 @@ import de.justjanne.libquassel.protocol.variant.indexed
 import de.justjanne.libquassel.protocol.variant.into
 import de.justjanne.libquassel.protocol.variant.qVariant
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 import org.threeten.bp.Instant
 import org.threeten.bp.temporal.Temporal
 
@@ -307,7 +306,7 @@ class IrcUser(
   inline fun state() = flow().value
 
   @Suppress("NOTHING_TO_INLINE")
-  inline fun flow(): StateFlow<IrcUserState> = state
+  inline fun flow() = state
 
   @PublishedApi
   internal val state = MutableStateFlow(
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt
index 9fdd6fe259aa78efc09c273682f81e1ed279ecbc..80bbef5781fd70c4586be3a4b69df7bdd4e2e846 100644
--- a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/Network.kt
@@ -32,7 +32,6 @@ import de.justjanne.libquassel.protocol.variant.QVariantMap
 import de.justjanne.libquassel.protocol.variant.into
 import de.justjanne.libquassel.protocol.variant.qVariant
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 import java.nio.ByteBuffer
 
 class Network constructor(
@@ -727,7 +726,7 @@ class Network constructor(
   inline fun state() = flow().value
 
   @Suppress("NOTHING_TO_INLINE")
-  inline fun flow(): StateFlow<NetworkState> = state
+  inline fun flow() = state
 
   @PublishedApi
   internal val state = MutableStateFlow(
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/NetworkConfig.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/NetworkConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30254bc6ea1c52a76d06cff7e300ecf560db246b
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/NetworkConfig.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferSyncerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferSyncerState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferSyncerState.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewConfigState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewConfigState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewConfigState.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewManagerState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/BufferViewManagerState.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CertManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CertManagerState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c129d46671bf0261c42719309a463cf2bc60ab15
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CertManagerState.kt
@@ -0,0 +1,23 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
+
+import de.justjanne.libquassel.protocol.models.ids.IdentityId
+import java.security.PrivateKey
+import java.security.cert.Certificate
+
+data class CertManagerState(
+  val identityId: IdentityId,
+  val privateKeyPem: String = "",
+  val certificatePem: String = "",
+  val privateKey: PrivateKey? = null,
+  val certificate: Certificate? = null
+)
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CoreInfoState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CoreInfoState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f09dbd464b16701a76c7cec3fee700cf3a54e71e
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/CoreInfoState.kt
@@ -0,0 +1,22 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
+
+import de.justjanne.libquassel.protocol.models.ConnectedClient
+import org.threeten.bp.Instant
+
+data class CoreInfoState(
+  val version: String = "",
+  val versionDate: Instant? = null,
+  val startTime: Instant = Instant.EPOCH,
+  val connectedClientCount: Int = 0,
+  val connectedClients: List<ConnectedClient> = emptyList()
+)
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/DccConfigState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/DccConfigState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..09f14fdf64c28a27b69ce2bc518b14952f1fb7cf
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/DccConfigState.kt
@@ -0,0 +1,28 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
+
+import de.justjanne.libquassel.protocol.models.DccIpDetectionMode
+import de.justjanne.libquassel.protocol.models.DccPortSelectionMode
+import java.net.InetAddress
+
+data class DccConfigState(
+  val dccEnabled: Boolean = false,
+  val outgoingIp: InetAddress = InetAddress.getLocalHost(),
+  val ipDetectionMode: DccIpDetectionMode = DccIpDetectionMode.Automatic,
+  val portSelectionMode: DccPortSelectionMode = DccPortSelectionMode.Automatic,
+  val minPort: UShort = 1024u,
+  val maxPort: UShort = 32767u,
+  val chunkSize: Int = 16,
+  val sendTimeout: Int = 180,
+  val usePassiveDcc: Boolean = false,
+  val useFastSend: Boolean = false
+)
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/HighlightRuleManagerState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/HighlightRuleManagerState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6401a56f3b4616d5c72bb631dbd8ce312ffc9a9a
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/HighlightRuleManagerState.kt
@@ -0,0 +1,87 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
+
+import de.justjanne.libquassel.protocol.models.HighlightNickType
+import de.justjanne.libquassel.protocol.models.HighlightRule
+import de.justjanne.libquassel.protocol.models.flags.MessageFlag
+import de.justjanne.libquassel.protocol.models.flags.MessageFlags
+import de.justjanne.libquassel.protocol.models.flags.MessageType
+import de.justjanne.libquassel.protocol.models.flags.MessageTypes
+import de.justjanne.libquassel.protocol.util.expression.ExpressionMatch
+import de.justjanne.libquassel.protocol.util.irc.IrcFormatDeserializer
+
+data class HighlightRuleManagerState(
+  val rules: List<HighlightRule> = emptyList(),
+  val highlightNickType: HighlightNickType = HighlightNickType.CurrentNick,
+  val highlightNickCaseSensitive: Boolean = false
+) {
+  fun match(
+    message: String,
+    sender: String,
+    type: MessageTypes,
+    flags: MessageFlags,
+    bufferName: String,
+    currentNick: String,
+    identityNicks: List<String>
+  ): Boolean {
+    val messageContent = IrcFormatDeserializer.stripColors(message)
+
+    if (!type.contains(MessageType.Action) &&
+      !type.contains(MessageType.Notice) &&
+      !type.contains(MessageType.Plain)
+    ) {
+      return false
+    }
+
+    if (flags.contains(MessageFlag.Self)) {
+      return false
+    }
+
+    val matchingRules = rules.asSequence()
+      .filter { it.isEnabled }
+      .filter { it.channelMatch.match(bufferName, true) }
+      .filter { it.senderMatch.match(sender, true) }
+      .filter { it.contentMatch.match(messageContent, true) }
+      .toList()
+
+    if (matchingRules.any(HighlightRule::isInverse)) {
+      return false
+    }
+
+    if (matchingRules.isNotEmpty()) {
+      return true
+    }
+
+    val nicks = when (highlightNickType) {
+      HighlightNickType.NoNick -> return false
+      HighlightNickType.CurrentNick -> listOf(currentNick)
+      HighlightNickType.AllNicks -> identityNicks + currentNick
+    }.filter(String::isNotBlank)
+
+    if (nicks.isNotEmpty() && ExpressionMatch(
+        nicks.joinToString("\n"),
+        ExpressionMatch.MatchMode.MatchMultiPhrase,
+        highlightNickCaseSensitive
+      ).match(messageContent)
+    ) {
+      return true
+    }
+
+    return false
+  }
+
+  fun indexOf(id: Int): Int = rules.indexOfFirst { it.id == id }
+  fun contains(id: Int) = rules.any { it.id == id }
+
+  fun isEmpty() = rules.isEmpty()
+  fun count() = rules.size
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IdentityState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IdentityState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IdentityState.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcListHelperState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcListHelperState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/IrcListHelperState.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkConfigState.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkConfigState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..182244f9d68a95c102cf5122edc7dec5c5d06394
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/syncables/state/NetworkConfigState.kt
@@ -0,0 +1,11 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.syncables.state
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatch.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatch.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b13da211c7d48f8893aace02cb458f3816d57e3c
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatch.kt
@@ -0,0 +1,259 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.util.expression
+
+/**
+ * Construct an Expression match with the given parameters
+ *
+ * @param expression  A phrase, wildcard expression, or regular expression
+ * @param mode     Expression matching mode @see ExpressionMatch.MatchMode
+ * @param caseSensitive If true, match case-sensitively, otherwise ignore case when matching
+ */
+data class ExpressionMatch(
+  val expression: String,
+  val mode: MatchMode,
+  val caseSensitive: Boolean
+) {
+  enum class MatchMode {
+    MatchPhrase,
+    MatchMultiPhrase,
+    MatchWildcard,
+    MatchMultiWildcard,
+    MatchRegEx
+  }
+
+  internal val positiveRegex: Regex?
+  internal val negativeRegex: Regex?
+
+  init {
+    val (positive, negative) = when (mode) {
+      MatchMode.MatchPhrase -> parsePhrase(expression)
+      MatchMode.MatchMultiPhrase -> parseMultiPhrase(expression)
+      MatchMode.MatchWildcard -> parseWildcard(expression)
+      MatchMode.MatchMultiWildcard -> parseMultiWildcard(expression)
+      MatchMode.MatchRegEx -> parseRegEx(expression)
+    }
+
+    positiveRegex = regex(positive, caseSensitive = caseSensitive)
+    negativeRegex = regex(negative, caseSensitive = caseSensitive)
+  }
+
+  fun isEmpty() = positiveRegex == null && negativeRegex == null
+
+  fun match(content: String, matchEmpty: Boolean = false): Boolean {
+    if (isEmpty()) {
+      return matchEmpty
+    }
+
+    if (negativeRegex != null && negativeRegex.containsMatchIn(content)) {
+      return false
+    }
+
+    if (positiveRegex != null) {
+      return positiveRegex.containsMatchIn(content)
+    }
+
+    return true
+  }
+
+  companion object {
+    private fun regex(expression: String, caseSensitive: Boolean): Regex? = try {
+      when {
+        expression.isBlank() -> null
+        caseSensitive -> expression.toRegex()
+        else -> expression.toRegex(RegexOption.IGNORE_CASE)
+      }
+    } catch (t: Throwable) {
+      null
+    }
+
+    internal fun splitWithEscaping(expression: String): List<String> {
+      val result = mutableListOf<String>()
+      val current = StringBuilder()
+      var escaped = false
+      for (char in expression) {
+        if (escaped) {
+          when (char) {
+            '\\' -> {
+              current.append(char)
+              current.append(char)
+              escaped = false
+            }
+            '\n' -> {
+              current.append("\\")
+              result.add(current.toString())
+              current.clear()
+              escaped = false
+            }
+            else -> {
+              current.append("\\")
+              current.append(char)
+              escaped = false
+            }
+          }
+        } else {
+          when (char) {
+            ';', '\n' -> {
+              result.add(current.toString())
+              current.clear()
+            }
+            '\\' -> {
+              escaped = true
+            }
+            else -> {
+              current.append(char)
+            }
+          }
+        }
+      }
+      result.add(current.toString())
+      return result
+    }
+
+    private fun parseInverted(expression: String): Pair<Boolean, String> {
+      if (expression.startsWith('!')) {
+        return Pair(true, expression.substring(1))
+      }
+
+      if (expression.startsWith("\\!")) {
+        return Pair(false, expression.substring(1))
+      }
+
+      return Pair(false, expression)
+    }
+
+    private fun escape(expression: String): String {
+      val result = StringBuilder(expression.length)
+      for (char in expression) {
+        when (char) {
+          '\\', '.', '[', ']', '{', '}', '(', ')', '<', '>', '*', '+', '-', '=', '?', '^', '$', '|' -> {
+            result.append('\\')
+            result.append(char)
+          }
+          else -> {
+            result.append(char)
+          }
+        }
+      }
+      return result.toString()
+    }
+
+    private fun parsePhrase(expression: String): Pair<String, String> {
+      if (expression.isBlank()) {
+        return Pair("", "")
+      }
+
+      return Pair("(?:^|\\W)${escape(expression)}(?:\\W|$)", "")
+    }
+
+    private fun parseMultiPhrase(expression: String): Pair<String, String> {
+      val components = expression.split('\n')
+        .filter(String::isNotEmpty)
+        .map(::escape)
+
+      if (components.isEmpty()) {
+        return Pair("", "")
+      }
+
+      return Pair(components.joinToString("|", prefix = "(?:^|\\W)(?:", postfix = ")(?:\\W|$)"), "")
+    }
+
+    private fun parseWildcardInternal(expression: String): String {
+      val result = StringBuilder()
+      var escaped = false
+      for (char in expression) {
+        when (char) {
+          '\\' -> if (escaped) {
+            result.append('\\')
+            result.append(char)
+            escaped = false
+          } else {
+            escaped = true
+          }
+          '?' -> if (escaped) {
+            result.append('\\')
+            result.append(char)
+            escaped = false
+          } else {
+            result.append('.')
+            escaped = false
+          }
+          '*' -> if (escaped) {
+            result.append('\\')
+            result.append(char)
+            escaped = false
+          } else {
+            result.append(".*")
+            escaped = false
+          }
+          '.', '[', ']', '{', '}', '(', ')', '<', '>', '+', '-', '=', '^', '$', '|' -> {
+            result.append('\\')
+            result.append(char)
+            escaped = false
+          }
+          else -> {
+            result.append(char)
+            escaped = false
+          }
+        }
+      }
+      return result.toString()
+    }
+
+    private fun parseWildcard(expression: String): Pair<String, String> {
+      val (inverted, phrase) = parseInverted(expression)
+      val result = "^${parseWildcardInternal(phrase)}$"
+
+      return if (inverted) Pair("", result)
+      else Pair(result, "")
+    }
+
+    private fun parseMultiWildcard(expression: String): Pair<String, String> {
+      val components = splitWithEscaping(expression)
+
+      val positive = components
+        .asSequence()
+        .map(::parseInverted)
+        .filterNot(Pair<Boolean, String>::first)
+        .map(Pair<Boolean, String>::second)
+        .map(String::trim)
+        .map(::parseWildcardInternal)
+        .filter(String::isNotEmpty)
+        .toList()
+
+      val negative = components
+        .asSequence()
+        .map(::parseInverted)
+        .filter(Pair<Boolean, String>::first)
+        .map(Pair<Boolean, String>::second)
+        .map(String::trim)
+        .map(::parseWildcardInternal)
+        .filter(String::isNotEmpty)
+        .toList()
+
+      return Pair(
+        if (positive.isEmpty()) ""
+        else positive.joinToString("|", prefix = "^(?:", postfix = ")$"),
+        if (negative.isEmpty()) ""
+        else negative.joinToString("|", prefix = "^(?:", postfix = ")$")
+      )
+    }
+
+    private fun parseRegEx(expression: String): Pair<String, String> {
+      val (inverted, phrase) = parseInverted(expression)
+      return when {
+        phrase.isBlank() -> return Pair("", "")
+        inverted -> Pair("", phrase)
+        else -> Pair(phrase, "")
+      }
+    }
+  }
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfo.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b2ab49bf9d0b9311dc8ae2f698c38b3fe1d2c14
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfo.kt
@@ -0,0 +1,16 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.util.irc
+
+data class FormatInfo<T : IrcFormat>(
+  val range: IntRange,
+  val format: T
+)
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfoBuilder.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfoBuilder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be2a6246178fcc913d528a66a7324422cd03d461
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/FormatInfoBuilder.kt
@@ -0,0 +1,16 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.util.irc
+
+data class FormatInfoBuilder<T : IrcFormat>(
+  val start: Int,
+  val format: T
+)
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormat.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormat.kt
new file mode 100644
index 0000000000000000000000000000000000000000..db64adde33b3f867d7defd12fcd5d008abb54c80
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormat.kt
@@ -0,0 +1,35 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.util.irc
+
+sealed class IrcFormat {
+  object Italic : IrcFormat()
+
+  object Underline : IrcFormat()
+
+  object Strikethrough : IrcFormat()
+
+  object Monospace : IrcFormat()
+
+  object Bold : IrcFormat()
+
+  data class HexColor(
+    val foreground: UInt,
+    val background: UInt
+  ) : IrcFormat()
+
+  data class IrcColor(
+    val foreground: UByte,
+    val background: UByte
+  ) : IrcFormat() {
+    fun copySwapped() = copy(foreground = background, background = foreground)
+  }
+}
diff --git a/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormatDeserializer.kt b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormatDeserializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..69ea0034c990738edd3fc4b9d83f239b5fd8230f
--- /dev/null
+++ b/libquassel-protocol/src/main/kotlin/de/justjanne/libquassel/protocol/util/irc/IrcFormatDeserializer.kt
@@ -0,0 +1,379 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2020 Janne Mareike Koschinski
+ * Copyright (c) 2020 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.justjanne.libquassel.protocol.util.irc
+
+object IrcFormatDeserializer {
+  fun stripColors(
+    str: String
+  ) = formatString(str, false, mutableListOf())
+
+  fun formatString(
+    str: String,
+    colorize: Boolean
+  ): Pair<String, List<FormatInfo<IrcFormat>>> {
+    val list = mutableListOf<FormatInfo<IrcFormat>>()
+    val content = formatString(str, colorize, list)
+    return Pair(content, list)
+  }
+
+  fun formatString(
+    str: String?,
+    colorize: Boolean,
+    output: MutableList<FormatInfo<IrcFormat>>
+  ): String {
+    if (str == null) return ""
+
+    val plainText = StringBuilder()
+    var bold: FormatInfoBuilder<IrcFormat.Bold>? = null
+    var italic: FormatInfoBuilder<IrcFormat.Italic>? = null
+    var underline: FormatInfoBuilder<IrcFormat.Underline>? = null
+    var strikethrough: FormatInfoBuilder<IrcFormat.Strikethrough>? = null
+    var monospace: FormatInfoBuilder<IrcFormat.Monospace>? = null
+    var color: FormatInfoBuilder<IrcFormat.IrcColor>? = null
+    var hexColor: FormatInfoBuilder<IrcFormat.HexColor>? = null
+
+    fun applyFormat(desc: FormatInfoBuilder<*>) {
+      output.add(FormatInfo(desc.start..plainText.length, desc.format))
+    }
+
+    // Iterating over every character
+    var normalCount = 0
+    var i = 0
+    while (i < str.length) {
+      val character = str[i]
+      when (character) {
+        CODE_BOLD -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          bold = if (bold != null) {
+            if (colorize) applyFormat(bold)
+            null
+            // Otherwise create a new one
+          } else {
+            FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.Bold
+            )
+          }
+        }
+        CODE_ITALIC -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          italic = if (italic != null) {
+            if (colorize) applyFormat(italic)
+            null
+            // Otherwise create a new one
+          } else {
+            FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.Italic
+            )
+          }
+        }
+        CODE_UNDERLINE -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          underline = if (underline != null) {
+            if (colorize) applyFormat(underline)
+            null
+            // Otherwise create a new one
+          } else {
+            FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.Underline
+            )
+          }
+        }
+        CODE_STRIKETHROUGH -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          strikethrough = if (strikethrough != null) {
+            if (colorize) applyFormat(strikethrough)
+            null
+            // Otherwise create a new one
+          } else {
+            FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.Strikethrough
+            )
+          }
+        }
+        CODE_MONOSPACE -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          monospace = if (monospace != null) {
+            if (colorize) applyFormat(monospace)
+            null
+            // Otherwise create a new one
+          } else {
+            FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.Monospace
+            )
+          }
+        }
+        CODE_COLOR -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          val foregroundStart = i + 1
+          val foregroundEnd = findEndOfNumber(str, foregroundStart)
+          // If we have a foreground element
+          if (foregroundEnd > foregroundStart) {
+            val foreground = readNumber(str, foregroundStart, foregroundEnd)
+
+            var background: UByte? = null
+            var backgroundEnd = -1
+            // If we have a background code, read it
+            if (str.length > foregroundEnd && str[foregroundEnd] == ',') {
+              backgroundEnd = findEndOfNumber(str, foregroundEnd + 1)
+              background = readNumber(str, foregroundEnd + 1, backgroundEnd)
+            }
+            // If previous element was also a color element, try to reuse background
+            if (color != null) {
+              // Apply old format
+              if (colorize) applyFormat(color)
+              // Reuse old background, if possible
+              if (background == null)
+                background = color.format.background
+            }
+            // Add new format
+            color = FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.IrcColor(
+                foreground ?: 0xFFu,
+                background ?: 0xFFu
+              )
+            )
+
+            // i points in front of the next character
+            i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1
+
+            // Otherwise assume this is a closing tag
+          } else if (color != null) {
+            if (colorize) applyFormat(color)
+            color = null
+          }
+        }
+        CODE_HEXCOLOR -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          val foregroundStart = i + 1
+          val foregroundEnd = findEndOfHexNumber(str, foregroundStart)
+          // If we have a foreground element
+          if (foregroundEnd > foregroundStart) {
+            val foreground = readHexNumber(str, foregroundStart, foregroundEnd)
+
+            var background: UInt? = null
+            var backgroundEnd = -1
+            // If we have a background code, read it
+            if (str.length > foregroundEnd && str[foregroundEnd] == ',') {
+              backgroundEnd = findEndOfHexNumber(str, foregroundEnd + 1)
+              background = readHexNumber(str, foregroundEnd + 1, backgroundEnd)
+            }
+            // If previous element was also a color element, try to reuse background
+            if (hexColor != null) {
+              // Apply old format
+              if (colorize) applyFormat(hexColor)
+              // Reuse old background, if possible
+              if (background == null)
+                background = hexColor.format.background
+            }
+            // Add new format
+            hexColor = FormatInfoBuilder(
+              plainText.length,
+              IrcFormat.HexColor(
+                foreground ?: 0xFFFFFFFFu,
+                background ?: 0xFFFFFFFFu
+              )
+            )
+
+            // i points in front of the next character
+            i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1
+
+            // Otherwise assume this is a closing tag
+          } else if (hexColor != null) {
+            if (colorize) applyFormat(hexColor)
+            hexColor = null
+          }
+        }
+        CODE_SWAP -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If we have a color tag before, apply it, and create a new one with swapped colors
+          if (color != null) {
+            if (colorize) applyFormat(color)
+            color = FormatInfoBuilder(
+              plainText.length, color.format.copySwapped()
+            )
+          }
+        }
+        CODE_RESET -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // End all formatting tags
+          if (bold != null) {
+            if (colorize) applyFormat(bold)
+            bold = null
+          }
+          if (italic != null) {
+            if (colorize) applyFormat(italic)
+            italic = null
+          }
+          if (underline != null) {
+            if (colorize) applyFormat(underline)
+            underline = null
+          }
+          if (color != null) {
+            if (colorize) applyFormat(color)
+            color = null
+          }
+          if (hexColor != null) {
+            if (colorize) applyFormat(hexColor)
+            hexColor = null
+          }
+        }
+        else -> {
+          // Just append it, if it’s not special
+          normalCount++
+        }
+      }
+      i++
+    }
+
+    plainText.append(str.substring(str.length - normalCount, str.length))
+
+    // End all formatting tags
+    if (bold != null) {
+      if (colorize) applyFormat(bold)
+    }
+    if (italic != null) {
+      if (colorize) applyFormat(italic)
+    }
+    if (underline != null) {
+      if (colorize) applyFormat(underline)
+    }
+    if (strikethrough != null) {
+      if (colorize) applyFormat(strikethrough)
+    }
+    if (monospace != null) {
+      if (colorize) applyFormat(monospace)
+    }
+    if (color != null) {
+      if (colorize) applyFormat(color)
+    }
+    if (hexColor != null) {
+      if (colorize) applyFormat(hexColor)
+    }
+    return plainText.toString()
+  }
+
+  private const val CODE_BOLD = 0x02.toChar()
+  private const val CODE_COLOR = 0x03.toChar()
+  private const val CODE_HEXCOLOR = 0x04.toChar()
+  private const val CODE_ITALIC = 0x1D.toChar()
+  private const val CODE_UNDERLINE = 0x1F.toChar()
+  private const val CODE_STRIKETHROUGH = 0x1E.toChar()
+  private const val CODE_MONOSPACE = 0x11.toChar()
+  private const val CODE_SWAP = 0x16.toChar()
+  private const val CODE_RESET = 0x0F.toChar()
+
+  /**
+   * Try to read a number from a String in specified bounds
+   *
+   * @param str   String to be read from
+   * @param start Start index (inclusive)
+   * @param end   End index (exclusive)
+   * @return The byte represented by the digits read from the string
+   */
+  fun readNumber(str: String, start: Int, end: Int): UByte? {
+    val result = str.substring(start, end)
+    return if (result.isEmpty()) null
+    else result.toUByteOrNull(10)
+  }
+
+  /**
+   * Try to read a number from a String in specified bounds
+   *
+   * @param str   String to be read from
+   * @param start Start index (inclusive)
+   * @param end   End index (exclusive)
+   * @return The byte represented by the digits read from the string
+   */
+  fun readHexNumber(str: String, start: Int, end: Int): UInt? {
+    val result = str.substring(start, end)
+    return if (result.isEmpty()) null
+    else result.toUIntOrNull(16)
+  }
+
+  /**
+   * @param str   String to be searched in
+   * @param start Start position (inclusive)
+   * @return Index of first character that is not a digit
+   */
+  private fun findEndOfNumber(str: String, start: Int): Int {
+    val searchFrame = str.substring(start)
+    var i = 0
+    loop@ while (i < 2 && i < searchFrame.length) {
+      when (searchFrame[i]) {
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
+          // Do nothing
+        }
+        else -> break@loop
+      }
+      i++
+    }
+    return start + i
+  }
+
+  /**
+   * @param str   String to be searched in
+   * @param start Start position (inclusive)
+   * @return Index of first character that is not a digit
+   */
+  private fun findEndOfHexNumber(str: String, start: Int): Int {
+    val searchFrame = str.substring(start)
+    var i = 0
+    loop@ while (i < 6 && i < searchFrame.length) {
+      when (searchFrame[i]) {
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'a', 'b',
+        'c', 'd', 'e', 'f' -> {
+          // Do nothing
+        }
+        else -> break@loop
+      }
+      i++
+    }
+    return start + i
+  }
+}
diff --git a/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatchTest.kt b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatchTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a485e5a33a74747638830c145a23b3aeaef42037
--- /dev/null
+++ b/libquassel-protocol/src/test/kotlin/de/justjanne/libquassel/protocol/util/expression/ExpressionMatchTest.kt
@@ -0,0 +1,477 @@
+/*
+ * libquassel
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package de.justjanne.libquassel.protocol.util.expression
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+class ExpressionMatchTest {
+  @Test
+  fun testEmptyPattern() {
+    // Empty pattern
+    val emptyMatch = ExpressionMatch("", ExpressionMatch.MatchMode.MatchPhrase, false)
+    assertNull(emptyMatch.positiveRegex)
+    assertNull(emptyMatch.negativeRegex)
+    // Assert empty is valid
+    // Assert empty
+    assertTrue(emptyMatch.isEmpty())
+    // Assert default match fails (same as setting match empty to false)
+    assertFalse(emptyMatch.match("something"))
+    // Assert match empty succeeds
+    assertTrue(emptyMatch.match("something", true))
+  }
+
+  @Test
+  fun testMatchPhrase() {
+    // Simple phrase, case-insensitive
+    val simpleMatch = ExpressionMatch("test", ExpressionMatch.MatchMode.MatchPhrase, false)
+    // Simple phrase, case-sensitive
+    val simpleMatchCS = ExpressionMatch("test", ExpressionMatch.MatchMode.MatchPhrase, true)
+    // Phrase with space, case-insensitive
+    val simpleMatchSpace = ExpressionMatch(" space ", ExpressionMatch.MatchMode.MatchPhrase, true)
+    // Complex phrase
+    val complexMatchFull = """^(?:norm|norm\-space|\!norm\-escaped|\\\!slash\-invert|\\\\double|escape\;""" +
+      """sep|slash\-end\-split\\|quad\\\\\!noninvert|newline\-split|newline\-split\-slash\\|slash\-at\-end\\)$"""
+    val complexMatch = ExpressionMatch(
+      complexMatchFull,
+      ExpressionMatch.MatchMode.MatchPhrase,
+      false
+    )
+
+    assertEquals("(?:^|\\W)test(?:\\W|$)", simpleMatch.positiveRegex?.pattern)
+    assertEquals(setOf(RegexOption.IGNORE_CASE), simpleMatch.positiveRegex?.options)
+    assertNull(simpleMatch.negativeRegex)
+
+    assertEquals("(?:^|\\W)test(?:\\W|$)", simpleMatchCS.positiveRegex?.pattern)
+    assertEquals(emptySet<RegexOption>(), simpleMatchCS.positiveRegex?.options)
+    assertNull(simpleMatchCS.negativeRegex)
+
+    assertEquals("(?:^|\\W) space (?:\\W|$)", simpleMatchSpace.positiveRegex?.pattern)
+    assertEquals(emptySet<RegexOption>(), simpleMatchSpace.positiveRegex?.options)
+    assertNull(simpleMatchSpace.negativeRegex)
+
+    // Assert valid and not empty
+    assertFalse(simpleMatch.isEmpty())
+    assertFalse(simpleMatchCS.isEmpty())
+    assertFalse(simpleMatchSpace.isEmpty())
+    assertFalse(complexMatch.isEmpty())
+
+    // Assert match succeeds
+    assertTrue(simpleMatch.match("test"))
+    assertTrue(simpleMatch.match("other test;"))
+    assertTrue(simpleMatchSpace.match(" space "))
+    // Assert partial match fails
+    assertFalse(simpleMatch.match("testing"))
+    assertFalse(simpleMatchSpace.match("space"))
+    // Assert unrelated fails
+    assertFalse(simpleMatch.match("not above"))
+
+    // Assert case sensitivity followed
+    assertFalse(simpleMatch.caseSensitive)
+    assertTrue(simpleMatch.match("TeSt"))
+    assertTrue(simpleMatchCS.caseSensitive)
+    assertFalse(simpleMatchCS.match("TeSt"))
+
+    // Assert complex phrases are escaped properly
+    assertTrue(complexMatch.match(complexMatchFull))
+    assertFalse(complexMatch.match("norm"))
+  }
+
+  @Test
+  fun matchMultiPhrase() {
+    val emptyMatch =
+      ExpressionMatch(
+        "\n\n",
+        ExpressionMatch.MatchMode.MatchMultiPhrase, false
+      )
+    // Simple phrases, case-insensitive
+    val simpleMatch = ExpressionMatch(
+      "test\nOther ",
+      ExpressionMatch.MatchMode.MatchMultiPhrase,
+      false
+    )
+    // Simple phrases, case-sensitive
+    val simpleMatchCS = ExpressionMatch(
+      "test\nOther ",
+      ExpressionMatch.MatchMode.MatchMultiPhrase,
+      true
+    )
+    // Complex phrases
+    val complexMatchFullA =
+      """^(?:norm|norm\-space|\!norm\-escaped|\\\!slash\-invert|\\\\double)|escape\;""" +
+        """sep|slash\-end\-split\\|quad\\\\\!noninvert)|newline\-split|newline\-split\-slash\\|slash\-at\-end\\)$"""
+    val complexMatchFullB = """^(?:invert|invert\-space)$)$"""
+    val complexMatch = ExpressionMatch(
+      complexMatchFullA + "\n" + complexMatchFullB,
+      ExpressionMatch.MatchMode.MatchMultiPhrase,
+      false
+    )
+
+    println(simpleMatch.positiveRegex)
+    println(simpleMatch.negativeRegex)
+
+    // Assert valid and not empty
+    assertFalse(simpleMatch.isEmpty())
+    assertFalse(simpleMatchCS.isEmpty())
+    assertFalse(complexMatch.isEmpty())
+
+    // Assert match succeeds
+    assertTrue(simpleMatch.match("test"))
+    assertTrue(simpleMatch.match("test[suffix]"))
+    assertTrue(simpleMatch.match("other test;"))
+    assertTrue(simpleMatch.match("Other "))
+    assertTrue(simpleMatch.match(".Other !"))
+    // Assert partial match fails
+    assertFalse(simpleMatch.match("testing"))
+    assertFalse(simpleMatch.match("Other!"))
+    // Assert unrelated fails
+    assertFalse(simpleMatch.match("not above"))
+
+    // Assert case sensitivity followed
+    assertFalse(simpleMatch.caseSensitive)
+    assertTrue(simpleMatch.match("TeSt"))
+    assertTrue(simpleMatchCS.caseSensitive)
+    assertFalse(simpleMatchCS.match("TeSt"))
+
+    // Assert complex phrases are escaped properly
+    assertTrue(complexMatch.match(complexMatchFullA))
+    assertTrue(complexMatch.match(complexMatchFullB))
+    assertFalse(complexMatch.match("norm"))
+    assertFalse(complexMatch.match("invert"))
+  }
+
+  @Test
+  fun matchWildcard() {
+    // Simple wildcard, case-insensitive
+    val simpleMatch =
+      ExpressionMatch("?test*", ExpressionMatch.MatchMode.MatchWildcard, false)
+    // Simple wildcard, case-sensitive
+    val simpleMatchCS =
+      ExpressionMatch("?test*", ExpressionMatch.MatchMode.MatchWildcard, true)
+    // Escaped wildcard, case-insensitive
+    val simpleMatchEscape =
+      ExpressionMatch("""\?test\*""", ExpressionMatch.MatchMode.MatchWildcard, false)
+    // Inverted wildcard, case-insensitive
+    val simpleMatchInvert =
+      ExpressionMatch("!test*", ExpressionMatch.MatchMode.MatchWildcard, false)
+    // Not inverted wildcard, case-insensitive
+    val simpleMatchNoInvert =
+      ExpressionMatch("""\!test*""", ExpressionMatch.MatchMode.MatchWildcard, false)
+    // Not inverted wildcard literal slash, case-insensitive
+    val simpleMatchNoInvertSlash =
+      ExpressionMatch("""\\!test*""", ExpressionMatch.MatchMode.MatchWildcard, false)
+    // Complex wildcard
+    val complexMatch =
+      ExpressionMatch(
+        """never?gonna*give\*you\?up\\test|y\yeah\\1\\\\2\\\1inval""",
+        ExpressionMatch.MatchMode.MatchWildcard, false
+      )
+
+    // Assert valid and not empty
+    assertFalse(simpleMatch.isEmpty())
+    assertFalse(simpleMatchCS.isEmpty())
+    assertFalse(simpleMatchEscape.isEmpty())
+    assertFalse(simpleMatchInvert.isEmpty())
+    assertFalse(simpleMatchNoInvert.isEmpty())
+    assertFalse(simpleMatchNoInvertSlash.isEmpty())
+    assertFalse(complexMatch.isEmpty())
+
+    // Assert match succeeds
+    assertTrue(simpleMatch.match("@test"))
+    assertTrue(simpleMatch.match("@testing"))
+    assertTrue(simpleMatch.match("!test"))
+    assertTrue(simpleMatchEscape.match("?test*"))
+    assertTrue(simpleMatchInvert.match("atest"))
+    assertTrue(simpleMatchNoInvert.match("!test"))
+    assertTrue(simpleMatchNoInvertSlash.match("""\!test)"""))
+    // Assert partial match fails
+    assertFalse(simpleMatch.match("test"))
+    // Assert unrelated fails
+    assertFalse(simpleMatch.match("not above"))
+    // Assert escaped wildcard fails
+    assertFalse(simpleMatchEscape.match("@testing"))
+    assertFalse(simpleMatchNoInvert.match("test"))
+    assertFalse(simpleMatchNoInvert.match("anything"))
+    assertFalse(simpleMatchNoInvertSlash.match("!test"))
+    assertFalse(simpleMatchNoInvertSlash.match("test"))
+    assertFalse(simpleMatchNoInvertSlash.match("anything"))
+    // Assert non-inverted fails
+    assertFalse(simpleMatchInvert.match("testing"))
+
+    // Assert case sensitivity followed
+    assertFalse(simpleMatch.caseSensitive)
+    assertTrue(simpleMatch.match("@TeSt"))
+    assertTrue(simpleMatchCS.caseSensitive)
+    assertFalse(simpleMatchCS.match("@TeSt"))
+
+    // Assert complex match
+    assertTrue(complexMatch.match("""neverAgonnaBBBgive*you?up\test|yyeah\1\\2\1inval"""))
+    // Assert complex not literal match
+    assertFalse(complexMatch.match("""never?gonna*give\*you\?up\\test|y\yeah\\1\\\\2\\\1inval"""))
+    // Assert complex unrelated not match
+    assertFalse(complexMatch.match("other"))
+  }
+
+  @Test
+  fun matchMultiWildcard() {
+    val emptyMatch =
+      ExpressionMatch(
+        ";!\n;",
+        ExpressionMatch.MatchMode.MatchMultiWildcard, false
+      )
+    val simpleMatch =
+      ExpressionMatch(
+        "?test*;another?",
+        ExpressionMatch.MatchMode.MatchMultiWildcard, false
+      )
+    val simpleMatchCS =
+      ExpressionMatch(
+        "?test*;another?",
+        ExpressionMatch.MatchMode.MatchMultiWildcard, true
+      )
+    val simpleMatchEscape =
+      ExpressionMatch(
+        "\\?test\\*\\\n*thing\\*",
+        ExpressionMatch.MatchMode.MatchMultiWildcard, false
+      )
+    val simpleMatchInvert =
+      ExpressionMatch(
+        """test*;!testing;\!testing""",
+        ExpressionMatch.MatchMode.MatchMultiWildcard, false
+      )
+    val simpleMatchImplicit =
+      ExpressionMatch(
+        """!testing*""",
+        ExpressionMatch.MatchMode.MatchMultiWildcard, false
+      )
+    val complexMatchFull = """norm;!invert; norm-space ; !invert-space ;;!;\!norm-escaped;""" +
+      """\\!slash-invert;\\\\double; escape\;sep;slash-end-split\\;""" +
+      """quad\\\\!noninvert;newline-split""" + "\n" +
+      """newline-split-slash\\""" + "\n" +
+      """slash-at-end\\"""
+
+    // Match normal components
+    val complexMatchNormal = listOf(
+      """norm""",
+      """norm-space""",
+      """!norm-escaped""",
+      """\!slash-invert""",
+      """\\double""",
+      """escape;sep""",
+      """slash-end-split\""",
+      """quad\\!noninvert""",
+      """newline-split""",
+      """newline-split-slash\""",
+      """slash-at-end\"""
+    )
+    // Match negating components
+    val complexMatchInvert = listOf(
+      """invert""",
+      """invert-space"""
+    )
+    val complexMatch = ExpressionMatch(
+      complexMatchFull, ExpressionMatch.MatchMode.MatchMultiWildcard,
+      false
+    )
+
+    // Assert valid and not empty
+    assertTrue(emptyMatch.isEmpty())
+    assertFalse(simpleMatch.isEmpty())
+    assertFalse(simpleMatchCS.isEmpty())
+    assertFalse(simpleMatchEscape.isEmpty())
+    assertFalse(simpleMatchInvert.isEmpty())
+    assertFalse(simpleMatchImplicit.isEmpty())
+    assertFalse(complexMatch.isEmpty())
+
+    // Assert match succeeds
+    assertTrue(simpleMatch.match("@test"))
+    assertTrue(simpleMatch.match("@testing"))
+    assertTrue(simpleMatch.match("!test"))
+    assertTrue(simpleMatch.match("anotherA"))
+    assertTrue(simpleMatchEscape.match("?test*;thing*"))
+    assertTrue(simpleMatchEscape.match("?test*;AAAAAthing*"))
+    assertTrue(simpleMatchInvert.match("test"))
+    assertTrue(simpleMatchInvert.match("testing things"))
+    // Assert implicit wildcard succeeds
+    assertTrue(simpleMatchImplicit.match("AAAAAA"))
+    // Assert partial match fails
+    assertFalse(simpleMatch.match("test"))
+    assertFalse(simpleMatch.match("another"))
+    assertFalse(simpleMatch.match("anotherBB"))
+    // Assert unrelated fails
+    assertFalse(simpleMatch.match("not above"))
+    // Assert escaped wildcard fails
+    assertFalse(simpleMatchEscape.match("@testing"))
+    // Assert inverted match fails
+    assertFalse(simpleMatchInvert.match("testing"))
+    assertFalse(simpleMatchImplicit.match("testing"))
+
+    // Assert case sensitivity followed
+    assertFalse(simpleMatch.caseSensitive)
+    assertTrue(simpleMatch.match("@TeSt"))
+    assertTrue(simpleMatchCS.caseSensitive)
+    assertFalse(simpleMatchCS.match("@TeSt"))
+
+    // Assert complex match
+    for (normMatch in complexMatchNormal) {
+      // Each normal component should match
+      assertTrue(complexMatch.match(normMatch))
+    }
+
+    for (invertMatch in complexMatchInvert) {
+      // Each invert component should not match
+      assertFalse(complexMatch.match(invertMatch))
+    }
+
+    // Assert complex not literal match
+    assertFalse(complexMatch.match(complexMatchFull))
+    // Assert complex unrelated not match
+    assertFalse(complexMatch.match("other"))
+  }
+
+  @Test
+  fun matchRegEx() {
+    val emptyMatch =
+      ExpressionMatch(
+        """ """,
+        ExpressionMatch.MatchMode.MatchRegEx, false
+      )
+    // Simple regex, case-insensitive
+    val simpleMatch =
+      ExpressionMatch(
+        """simple.\*escape-match.*""",
+        ExpressionMatch.MatchMode.MatchRegEx, false
+      )
+    // Simple regex, case-sensitive
+    val simpleMatchCS =
+      ExpressionMatch(
+        """simple.\*escape-match.*""",
+        ExpressionMatch.MatchMode.MatchRegEx, true
+      )
+    // Inverted regex, case-insensitive
+    val simpleMatchInvert =
+      ExpressionMatch(
+        """!invert.\*escape-match.*""",
+        ExpressionMatch.MatchMode.MatchRegEx, false
+      )
+    // Non-inverted regex, case-insensitive
+    val simpleMatchNoInvert =
+      ExpressionMatch(
+        """\!simple.\*escape-match.*""",
+        ExpressionMatch.MatchMode.MatchRegEx, false
+      )
+    // Non-inverted regex literal slash, case-insensitive
+    val simpleMatchNoInvertSlash =
+      ExpressionMatch(
+        """\\!simple.\*escape-match.*""",
+        ExpressionMatch.MatchMode.MatchRegEx, false
+      )
+
+    // Assert valid and not empty
+    assertTrue(emptyMatch.isEmpty())
+    assertFalse(simpleMatch.isEmpty())
+    assertFalse(simpleMatchCS.isEmpty())
+    assertFalse(simpleMatchInvert.isEmpty())
+    assertFalse(simpleMatchNoInvert.isEmpty())
+    assertFalse(simpleMatchNoInvertSlash.isEmpty())
+
+    // Assert match succeeds
+    assertTrue(simpleMatch.match("simpleA*escape-match"))
+    assertTrue(simpleMatch.match("simpleA*escape-matchBBBB"))
+    assertTrue(simpleMatchInvert.match("not above"))
+    assertTrue(simpleMatchNoInvert.match("!simpleA*escape-matchBBBB"))
+    assertTrue(simpleMatchNoInvertSlash.match("""\!simpleA*escape-matchBBBB"""))
+    // Assert partial match fails
+    assertFalse(simpleMatch.match("simpleA*escape-mat"))
+    assertFalse(simpleMatch.match("simple*escape-match"))
+    // Assert unrelated fails
+    assertFalse(simpleMatch.match("not above"))
+    // Assert escaped wildcard fails
+    assertFalse(simpleMatch.match("simpleABBBBescape-matchBBBB"))
+    // Assert inverted fails
+    assertFalse(simpleMatchInvert.match("invertA*escape-match"))
+    assertFalse(simpleMatchInvert.match("invertA*escape-matchBBBB"))
+    assertFalse(simpleMatchNoInvert.match("simpleA*escape-matchBBBB"))
+    assertFalse(simpleMatchNoInvert.match("anything"))
+    assertFalse(simpleMatchNoInvertSlash.match("!simpleA*escape-matchBBBB"))
+    assertFalse(simpleMatchNoInvertSlash.match("anything"))
+
+    // Assert case sensitivity followed
+    assertFalse(simpleMatch.caseSensitive)
+    assertTrue(simpleMatch.match("SiMpLEA*escape-MATCH"))
+    assertTrue(simpleMatchCS.caseSensitive)
+    assertFalse(simpleMatchCS.match("SiMpLEA*escape-MATCH"))
+  }
+
+  @Test
+  fun testInvalid() {
+    val invalidRegex = ExpressionMatch("*network", ExpressionMatch.MatchMode.MatchRegEx, false)
+    assertFalse(invalidRegex.match(""))
+    assertFalse(invalidRegex.match("network"))
+    assertFalse(invalidRegex.match("testnetwork"))
+  }
+
+  // Tests imported from https://github.com/ircdocs/parser-tests/blob/master/tests/mask-match.yaml
+  @Test
+  fun testDan() {
+    val mask1 = ExpressionMatch(
+      "*@127.0.0.1",
+      ExpressionMatch.MatchMode.MatchWildcard,
+      caseSensitive = false
+    )
+    println(mask1.positiveRegex)
+    assertTrue(mask1.match("coolguy!ab@127.0.0.1"))
+    assertTrue(mask1.match("cooldud3!~bc@127.0.0.1"))
+    assertFalse(mask1.match("coolguy!ab@127.0.0.5"))
+    assertFalse(mask1.match("cooldud3!~d@124.0.0.1"))
+
+    val mask2 = ExpressionMatch(
+      "cool*@*",
+      ExpressionMatch.MatchMode.MatchWildcard,
+      caseSensitive = false
+    )
+    println(mask2.positiveRegex)
+    assertTrue(mask2.match("coolguy!ab@127.0.0.1"))
+    assertTrue(mask2.match("cooldud3!~bc@127.0.0.1"))
+    assertTrue(mask2.match("cool132!ab@example.com"))
+    assertFalse(mask2.match("koolguy!ab@127.0.0.5"))
+    assertFalse(mask2.match("cooodud3!~d@124.0.0.1"))
+
+    val mask3 = ExpressionMatch(
+      "cool!*@*",
+      ExpressionMatch.MatchMode.MatchWildcard,
+      caseSensitive = false
+    )
+    println(mask3.positiveRegex)
+    assertTrue(mask3.match("cool!guyab@127.0.0.1"))
+    assertTrue(mask3.match("cool!~dudebc@127.0.0.1"))
+    assertTrue(mask3.match("cool!312ab@example.com"))
+    assertFalse(mask3.match("coolguy!ab@127.0.0.1"))
+    assertFalse(mask3.match("cooldud3!~bc@127.0.0.1"))
+    assertFalse(mask3.match("koolguy!ab@127.0.0.5"))
+    assertFalse(mask3.match("cooodud3!~d@124.0.0.1"))
+
+    // Cause failures in fnmatch/glob based matchers
+    val mask4 = ExpressionMatch(
+      "cool[guy]!*@*",
+      ExpressionMatch.MatchMode.MatchWildcard,
+      caseSensitive = false
+    )
+    println(mask4.positiveRegex)
+    assertTrue(mask4.match("cool[guy]!guy@127.0.0.1"))
+    assertTrue(mask4.match("cool[guy]!a@example.com"))
+    assertFalse(mask4.match("coolg!ab@127.0.0.1"))
+    assertFalse(mask4.match("cool[!ac@127.0.1.1"))
+  }
+}