git » sdk » commit bbd2bc4

Clean up e2ee preference system

author Stephen Paul Weber
2026-07-01 02:44:41 UTC
committer Stephen Paul Weber
2026-07-01 02:44:41 UTC
parent ce87df2ce1d34634d49010d383244e9ece36662b

Clean up e2ee preference system

HaxeSwiftBridge.hx +2 -0
borogove/Chat.hx +32 -30
borogove/Client.hx +59 -11
borogove/EncryptionInfo.hx +4 -4
borogove/EncryptionPolicy.hx +0 -15
borogove/OMEMO.hx +4 -3
borogove/persistence/IDB.js +1 -0
browserjs.hxml +0 -1
nodejs.hxml +0 -1
npm/index.ts +2 -0
test/TestChat.hx +48 -0
test/TestClient.hx +49 -0
testjs.hxml +0 -1

diff --git a/HaxeSwiftBridge.hx b/HaxeSwiftBridge.hx
index bb781a1..4dd452f 100644
--- a/HaxeSwiftBridge.hx
+++ b/HaxeSwiftBridge.hx
@@ -236,6 +236,8 @@ class HaxeSwiftBridge {
 					case EConst(CInt(i, null)):
 						builder.add(" = ");
 						builder.add(Std.string(i));
+					case EConst(CIdent(id)) if (id == "Default"):
+						builder.add(" = 0");
 					case null:
 						builder.add(" = nil");
 					default:
diff --git a/borogove/Chat.hx b/borogove/Chat.hx
index 025f755..42a939b 100644
--- a/borogove/Chat.hx
+++ b/borogove/Chat.hx
@@ -91,16 +91,17 @@ enum abstract UserState(Int) {
 }
 
 /**
-	Describes the current encryption mode of the conversation.
-
-	This mode is a high-level representation of the user/app *intent*
-	for the current conversation - e.g. not a guarantee that incoming
-	messages will always match this expectation. It is used to determine
-	the logic for outgoing messages, though.
+	End-to-End Encryption preferences for outgoing messages.
 **/
-enum abstract EncryptionMode(Int) {
-	var Unencrypted; // No end-to-end encryption
-	var EncryptedOMEMO; // Use OMEMO
+enum abstract OutgoingE2EEPreference(Int) {
+	/** Use the client's default allowed E2EE method. **/
+	var Default = 0;
+	/** Send the message without any End-to-End Encryption. **/
+	var NoE2EE;
+#if !NO_OMEMO
+	/** Use OMEMO for End-to-End Encryption. **/
+	var OMEMO;
+#end
 }
 
 @:expose
@@ -161,7 +162,6 @@ abstract class Chat extends EventEmitter {
 	private var activeThread: Null<String> = null;
 	private var notificationSettings: Null<{reply: Bool, mention: Bool}> = null;
 	private var outbox = new Outbox();
-	private var _encryptionMode: EncryptionMode = Unencrypted;
 
 	@:allow(borogove)
 	private var omemoContactDeviceIDs: Null<Array<Int>> = null;
@@ -253,10 +253,11 @@ abstract class Chat extends EventEmitter {
 		Send a message to this Chat
 
 		@param message the ChatMessageBuilder to send
+		@param e2eePreference optional explicit End-to-End Encryption preference for this message. If Default, uses the client's preferred allowed E2EE method.
 	**/
-	abstract public function sendMessage(message:ChatMessageBuilder):Void;
+	abstract public function sendMessage(message:ChatMessageBuilder, e2eePreference:OutgoingE2EEPreference = Default):Void;
 
-	abstract private function sendMessageStanza(stanza: Stanza, ?outboxItem: OutboxItem):Void;
+	abstract private function sendMessageStanza(stanza: Stanza, ?outboxItem: OutboxItem, e2eePreference: OutgoingE2EEPreference = Default):Void;
 
 	/**
 		Signals that all messages up to and including this one have probably
@@ -852,17 +853,6 @@ abstract class Chat extends EventEmitter {
 		return jingleSessions.flatMap((session) -> session.videoTracks());
 	}
 #end
-	/**
-		Get encryption mode for this chat
-	**/
-	public function encryptionMode(): String {
-		switch(_encryptionMode) {
-			case Unencrypted:
-				return "unencrypted";
-			case EncryptedOMEMO:
-				return "omemo";
-		}
-	}
 
 	/**
 		Can the user send messages to this chat?
@@ -1248,7 +1238,7 @@ class DirectChat extends Chat {
 	}
 
 	@HaxeCBridge.noemit // on superclass as abstract
-	public function sendMessage(message: ChatMessageBuilder):Void {
+	public function sendMessage(message: ChatMessageBuilder, e2eePreference: OutgoingE2EEPreference = Default):Void {
 		if (uiState == Invited) uiState = Open;
 		if (typingTimer != null) typingTimer.stop();
 		client.chatActivity(this);
@@ -1265,7 +1255,7 @@ class DirectChat extends Chat {
 						activeThread = message.threadId;
 						stanza.tag("active", { xmlns: "http://jabber.org/protocol/chatstates" }).up();
 					}
-					sendMessageStanza(stanza, outboxItem);
+					sendMessageStanza(stanza, outboxItem, e2eePreference);
 					setLastMessage(stored).then(_ -> {
 						client.notifyMessageHandlers(stored, stored.versions.length > 1 ? CorrectionEvent : DeliveryEvent);
 						client.trigger("chats/update", [this]);
@@ -1318,7 +1308,7 @@ class DirectChat extends Chat {
 		});
 	}
 
-	private function sendMessageStanza(stanza: Stanza, ?outboxItem: OutboxItem) {
+	private function sendMessageStanza(stanza: Stanza, ?outboxItem: OutboxItem, e2eePreference: OutgoingE2EEPreference = Default) {
 		if (stanza.name != "message") throw "Can only send message stanza this way";
 
 		if (outboxItem == null) outboxItem = outbox.newItem();
@@ -1335,12 +1325,24 @@ class DirectChat extends Chat {
 				addresses.up();
 			}
 
+			// Default should try all preferences in order?
+			// Otherwise force to the one we were passed?
+			if (e2eePreference == Default) e2eePreference = client.outgoingE2EEPreference[0];
 			#if NO_OMEMO
+			if (e2eePreference != NoE2EE) throw "Selected E2EE preference unsupported";
 			return Promise.resolve(stanza);
 			#else
-			return client.omemo.encryptMessage(JID.parse(counterpart), stanza).then((encryptedStanza) -> {
+			switch (e2eePreference) {
+			case OMEMO:
+				// As written this falls back to NoE2EE if allowed
+				return client.omemo.encryptMessage(JID.parse(counterpart), stanza).then((encryptedStanza) -> {
+					return Promise.resolve(encryptedStanza);
+				});
+			case NoE2EE:
 				return Promise.resolve(stanza);
-			});
+			case Default:
+				throw "Impossible";
+			}
 			#end
 		})).then(stanzas -> {
 			outboxItem.handle(() -> {
@@ -2271,7 +2273,7 @@ class Channel extends Chat {
 	}
 
 	@HaxeCBridge.noemit // on superclass as abstract
-	public function sendMessage(message:ChatMessageBuilder):Void {
+	public function sendMessage(message:ChatMessageBuilder, e2eePreference:OutgoingE2EEPreference = Default):Void {
 		if (uiState == Invited) uiState = Open;
 		if (typingTimer != null) typingTimer.stop();
 		client.chatActivity(this);
@@ -2341,7 +2343,7 @@ class Channel extends Chat {
 		});
 	}
 
-	private function sendMessageStanza(stanza: Stanza, ?outboxItem: OutboxItem) {
+	private function sendMessageStanza(stanza: Stanza, ?outboxItem: OutboxItem, e2eePreference: OutgoingE2EEPreference = Default) {
 		if (stanza.name != "message") throw "Can only send message stanza this way";
 
 		if (outboxItem == null) outboxItem = outbox.newItem();
diff --git a/borogove/Client.hx b/borogove/Client.hx
index 0a339f2..292bbfb 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -11,7 +11,6 @@ import borogove.Chat;
 import borogove.ChatMessage;
 import borogove.Message;
 import borogove.EventEmitter;
-import borogove.EncryptionPolicy;
 #if !NO_OMEMO
 import borogove.OMEMO;
 #end
@@ -65,6 +64,18 @@ class Client extends EventEmitter {
 	**/
 	@:allow(borogove)
 	public var sendAvailable(null, default): Bool = true;
+	/**
+		If true, the client will block and return a policy-violation error for
+		any incoming chat message that is not end-to-end encrypted.
+	**/
+	public var blockIncomingWithoutE2EE: Bool = false;
+	@:allow(borogove)
+	private var outgoingE2EEPreference: Array<OutgoingE2EEPreference> = [
+		NoE2EE,
+		#if !NO_OMEMO
+			OMEMO
+		#end
+	];
 	private var stream:GenericStream;
 	@:allow(borogove)
 	private var jid(default,null):JID;
@@ -108,12 +119,6 @@ class Client extends EventEmitter {
 	private var sortId: String = "a ";
 	private final pendingCaps: Map<String, Array<(Null<Caps>)->Chat>> = [];
 	private final brokenAvatars: Map<String, JID> = [];
-	@:allow(borogove)
-	private final encryptionPolicy:EncryptionPolicy = {
-		allowUnencryptedOutgoing: true,
-		allowUnencryptedIncoming: true,
-		preferEncryptedOutgoing: true,
-	};
 
 #if !NO_OMEMO
 	@:allow(borogove)
@@ -209,7 +214,7 @@ class Client extends EventEmitter {
 			}
 
 #if !NO_OMEMO
-			if((fwd??stanza).hasChild("encrypted", NS.OMEMO)) {
+			if((fwd??stanza).hasChild("encrypted", NS.OMEMO) && omemo != null) {
 				omemo.decryptMessage(stanza, fwd).then((decryptionResult) -> {
 					trace("OMEMO: Decrypted message, now processing...");
 					processLiveMessage(decryptionResult.stanza, fwd, decryptionResult.encryptionInfo);
@@ -218,6 +223,7 @@ class Client extends EventEmitter {
 				return EventHandled;
 			}
 #end
+
 			processLiveMessage(stanza, fwd);
 			return EventHandled;
 		});
@@ -482,6 +488,17 @@ class Client extends EventEmitter {
 
 		switch (message.parsed) {
 			case ChatMessageStanza(chatMessage):
+				if (blockIncomingWithoutE2EE && message.encryption == null) {
+					// return error for disallowed no-e2ee incoming chat message
+					sendStanza(
+						new Stanza("message", { type: "error", id: stanza.attr.get("id"), to: stanza.attr.get("from") })
+						.tag("error", { by: jid.asString(), type: "cancel" })
+						.tag("policy-violation", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+						.textTag("text", "E2EE Required", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" })
+					);
+					return;
+				}
+
 				for (hash in chatMessage.inlineHashReferences()) {
 					fetchMediaByHash([hash], [chatMessage.from]);
 				}
@@ -733,7 +750,7 @@ class Client extends EventEmitter {
 			trace("pubsubNode == "+pubsubNode);
 
 #if !NO_OMEMO
-			if(pubsubNode == "eu.siacs.conversations.axolotl.devicelist") {
+			if(pubsubNode == "eu.siacs.conversations.axolotl.devicelist" && omemo != null) {
 				if(isOwnAccount) {
 					omemo.onAccountUpdatedDeviceList(pubsubEvent.getItems());
 				} else {
@@ -988,6 +1005,27 @@ class Client extends EventEmitter {
 		return true;
 	}
 
+	/**
+		Adjusts the outgoing E2EE preference list to prioritize the specified preference.
+		This ensures that the client will attempt to use this E2EE protocol first when sending messages.
+
+		@param preference the OutgoingE2EEPreference to prioritize
+	**/
+	public function preferE2ee(preference: OutgoingE2EEPreference) {
+		outgoingE2EEPreference = outgoingE2EEPreference.filter(p -> p != preference);
+		outgoingE2EEPreference.unshift(preference);
+	}
+
+	/**
+		Removes the specified E2EE preference from the allowed outgoing preferences.
+		The client will no longer use this protocol for outgoing messages.
+
+		@param preference the OutgoingE2EEPreference to ban
+	**/
+	public function banE2ee(preference: OutgoingE2EEPreference) {
+		outgoingE2EEPreference = outgoingE2EEPreference.filter(p -> p != preference);
+	}
+
 	private function onConnected(data) { // Fired on connect or reconnect
 		if (data != null && data.jid != null) {
 			jid = JID.parse(data.jid);
@@ -2061,8 +2099,18 @@ class Client extends EventEmitter {
 			for (m in messageList.messages) {
 				switch (m.parsed) {
 					case ChatMessageStanza(message):
-						chatMessages.push(message);
-						if (message.type == MessageChat) chatIds[message.chatId()] = true;
+						if (blockIncomingWithoutE2EE && m.encryption == null) {
+							// return error for disallowed no-e2ee incoming chat message
+							sendStanza(
+								new Stanza("message", { type: "error", id: message.localId, to: message.from.asString() })
+								.tag("error", { by: jid.asString(), type: "cancel" })
+								.tag("policy-violation", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }).up()
+								.textTag("text", "E2EE Required", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" })
+							);
+						} else {
+							chatMessages.push(message);
+							if (message.type == MessageChat) chatIds[message.chatId()] = true;
+						}
 					case ReactionUpdateStanza(update):
 						promises.push(
 							persistence.storeReaction(accountId(), update).then(_ -> null)
diff --git a/borogove/EncryptionInfo.hx b/borogove/EncryptionInfo.hx
index 358eaa7..e8986b3 100644
--- a/borogove/EncryptionInfo.hx
+++ b/borogove/EncryptionInfo.hx
@@ -35,10 +35,10 @@ class EncryptionInfo {
 	];
 
 	@:allow(borogove)
-	private function new(status:EncryptionStatus, method:String, ?methodName:String, ?reason:String, ?reasonText:String) {
+	private function new(status:EncryptionStatus, method:String, ?reason:String, ?reasonText:String, ?methodName:String) {
 		this.status = status;
 		this.method = method;
-		this.methodName = methodName;
+		this.methodName = methodName ?? knownEncryptionSchemes.get(method);
 		this.reason = reason;
 		this.reasonText = reasonText;
 	}
@@ -79,9 +79,9 @@ class EncryptionInfo {
 			return new EncryptionInfo(
 				DecryptionFailure,
 				ns??"unknown",
-				knownEncryptionSchemes.get(ns)??name??"Unknown encryption",
 				"unsupported-encryption",
-				"Unsupported encryption method: "+(name??ns)
+				"Unsupported encryption method: "+(name??ns),
+				knownEncryptionSchemes.get(ns)??name??"Unknown encryption"
 			);
 		}
 		return null; // Probably not encrypted
diff --git a/borogove/EncryptionPolicy.hx b/borogove/EncryptionPolicy.hx
deleted file mode 100644
index 9f0a9b0..0000000
--- a/borogove/EncryptionPolicy.hx
+++ /dev/null
@@ -1,15 +0,0 @@
-package borogove;
-
-@:structInit
-class EncryptionPolicy {
-    // These allow blocking all incoming/outgoing
-    // chat messages which are not using E2EE
-	public final allowUnencryptedIncoming:Bool;
-	public final allowUnencryptedOutgoing:Bool;
-
-    // Outgoing encryption will be preferred,
-    // but may not be used if a recipient lacks
-    // support (and of course if the above
-    // policies permit unencrypted)
-    public final preferEncryptedOutgoing:Bool;
-}
diff --git a/borogove/OMEMO.hx b/borogove/OMEMO.hx
index 3cdb0e8..3ed722b 100644
--- a/borogove/OMEMO.hx
+++ b/borogove/OMEMO.hx
@@ -16,6 +16,7 @@ import thenshim.Promise;
 import thenshim.PromiseTools;
 
 using borogove.SignalProtocol;
+using Lambda;
 
 #if js
 import js.Browser;
@@ -1108,7 +1109,7 @@ class OMEMO {
 			return newStanza;
 		}, (failureReason) -> {
 			final noRecipientSupport = failureReason == "no-devices";
-			var allowUnencrypted:Bool = client.encryptionPolicy.allowUnencryptedOutgoing;
+			var allowUnencrypted:Bool = client.outgoingE2EEPreference.exists(p -> p == NoE2EE);
 
 			var errMsg:String;
 			if(noRecipientSupport) {
@@ -1120,7 +1121,7 @@ class OMEMO {
 				// encrypted communication *is* preferred, we need a good excuse to
 				// send unencrypted (such as no recipient support), but no such excuse
 				// is found here.
-				allowUnencrypted = allowUnencrypted && !client.encryptionPolicy.preferEncryptedOutgoing;
+				allowUnencrypted = allowUnencrypted && client.outgoingE2EEPreference[0] == NoE2EE;
 			}
 
 			if(!allowUnencrypted) {
@@ -1213,7 +1214,7 @@ class OMEMO {
 			// to a pair of bytes (since JS uses UTF-16).
 			return Browser.window.btoa(keyStr);
 		#else
-			return Base64.encode(bytesOfString(keyStr, RawNative));
+			return Base64.encode(bytesOfString(keyStr));
 		#end
 	}
 
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index e63d50b..1ae627b 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -343,6 +343,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 		message.lang = value.lang;
 		message.type = value.type || (value.isGroupchat || value.groupchat ? enums.borogove_MessageType.Channel : enums.borogove_MessageType.Chat);
 		message.payloads = (value.payloads || []).map(borogove_Stanza.parse);
+		message.encryption = value.encryption;
 		message.stanza = value.stanza && borogove_Stanza.parse(value.stanza);
 		if (!message.localId && !message.serverId) message.localId = "NO_ID"; // bad data
 		return message.build();
diff --git a/browserjs.hxml b/browserjs.hxml
index 942e9d8..170a7b1 100644
--- a/browserjs.hxml
+++ b/browserjs.hxml
@@ -23,5 +23,4 @@ borogove.Import
 -D hxtsdgen_enums_ts
 -D js_global=globalThis
 -D js-classic
--D NO_OMEMO
 --js npm/borogove-browser.js
diff --git a/nodejs.hxml b/nodejs.hxml
index cbc4efe..23aa506 100644
--- a/nodejs.hxml
+++ b/nodejs.hxml
@@ -26,5 +26,4 @@ borogove.Import
 -D hxtsdgen_enums_ts
 -D js_global=globalThis
 -D js-classic
--D NO_OMEMO
 --js npm/borogove.js
diff --git a/npm/index.ts b/npm/index.ts
index 3224dcf..a7095e7 100644
--- a/npm/index.ts
+++ b/npm/index.ts
@@ -1,8 +1,10 @@
 export {
 	borogove_ChatMessageEvent as ChatMessageEvent,
+	borogove_EncryptionStatus as EncryptionStatus,
 	borogove_MessageDirection as MessageDirection,
 	borogove_MessageStatus as MessageStatus,
 	borogove_MessageType as MessageType,
+	borogove_OutgoingE2EEPreference as OutgoingE2EEPreference,
 	borogove_ReactionUpdateKind as ReactionUpdateKind,
 	borogove_UiState as UiState,
 	borogove_UserState as UserState,
diff --git a/test/TestChat.hx b/test/TestChat.hx
index f816fdf..75745d3 100644
--- a/test/TestChat.hx
+++ b/test/TestChat.hx
@@ -735,6 +735,54 @@ class TestChat extends utest.Test {
 		chat.voiceRequestRespond(member, true);
 		chat.voiceRequestRespond(member, false);
 	}
+
+	public function testSendMessageNoE2EE(async: Async) {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		final chat = client.getDirectChat("friend@example.com");
+
+		client.stream.on("sendStanza", (stanza: Stanza) -> {
+			if (stanza.name == "message" && stanza.getChild("encrypted", "eu.siacs.conversations.axolotl") == null) {
+				Assert.equals("hello", stanza.getChildText("body"));
+				async.done();
+				return EventHandled;
+			}
+			return EventUnhandled;
+		});
+
+		final builder = new ChatMessageBuilder();
+		builder.text = "hello";
+		chat.sendMessage(builder, borogove.Chat.OutgoingE2EEPreference.NoE2EE);
+	}
+
+#if js
+	public function testSendMessageOMEMO(async: Async) {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		final chat = client.getDirectChat("friend@example.com");
+
+		Reflect.setField(client, "omemo", {
+			encryptMessage: function(jid: borogove.JID, stanza: Stanza) {
+				final encrypted = stanza.clone();
+				encrypted.tag("encrypted", { xmlns: "eu.siacs.conversations.axolotl" }).up();
+				return Promise.resolve(encrypted);
+			}
+		});
+
+		client.stream.on("sendStanza", (stanza: Stanza) -> {
+			if (stanza.name == "message" && stanza.getChild("encrypted", "eu.siacs.conversations.axolotl") != null) {
+				Assert.notNull(stanza.getChild("encrypted", "eu.siacs.conversations.axolotl"));
+				async.done();
+				return EventHandled;
+			}
+			return EventUnhandled;
+		});
+
+		final builder = new ChatMessageBuilder();
+		builder.text = "hello";
+		chat.sendMessage(builder, borogove.Chat.OutgoingE2EEPreference.OMEMO);
+	}
+#end
 }
 
 @:access(borogove)
diff --git a/test/TestClient.hx b/test/TestClient.hx
index 2f006a3..0241ca2 100644
--- a/test/TestClient.hx
+++ b/test/TestClient.hx
@@ -17,6 +17,7 @@ import borogove.Role;
 import borogove.Stanza;
 import borogove.Status;
 import borogove.persistence.Dummy;
+import borogove.Chat.OutgoingE2EEPreference;
 
 using Lambda;
 
@@ -637,6 +638,54 @@ class TestClient extends utest.Test {
 			async.done();
 		});
 	}
+
+	public function testPreferE2ee() {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		client.outgoingE2EEPreference = [];
+		client.preferE2ee(OutgoingE2EEPreference.NoE2EE);
+		Assert.equals(OutgoingE2EEPreference.NoE2EE, client.outgoingE2EEPreference[0]);
+	}
+
+	public function testBanE2ee() {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		client.banE2ee(OutgoingE2EEPreference.NoE2EE);
+		Assert.isFalse(client.outgoingE2EEPreference.contains(OutgoingE2EEPreference.NoE2EE));
+	}
+
+	public function testBlockIncomingWithoutE2EE(async: Async) {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		client.blockIncomingWithoutE2EE = true;
+
+		client.stream.on("sendStanza", (stanza: Stanza) -> {
+			if (stanza.name == "message" && stanza.attr.get("type") == "error") {
+				Assert.equals("friend@example.com", stanza.attr.get("to"));
+				final error = stanza.getChild("error");
+				Assert.notNull(error);
+				Assert.equals("cancel", error.attr.get("type"));
+				Assert.notNull(error.getChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas"));
+				async.done();
+				return EventHandled;
+			}
+			return EventUnhandled;
+		});
+
+		client.stream.onStanza(
+			new Stanza("message", { xmlns: "jabber:client", from: "friend@example.com", id: "msg1" })
+				.textTag("body", "hello without e2ee")
+		);
+	}
+
+#if js
+	public function testPreferOMEMO() {
+		final persistence = new Dummy();
+		final client = new Client("test@example.com", persistence);
+		client.preferE2ee(OutgoingE2EEPreference.OMEMO);
+		Assert.equals(OutgoingE2EEPreference.OMEMO, client.outgoingE2EEPreference[0]);
+	}
+#end
 }
 
 @:access(borogove)
diff --git a/testjs.hxml b/testjs.hxml
index a095639..84c3dc4 100644
--- a/testjs.hxml
+++ b/testjs.hxml
@@ -17,7 +17,6 @@
 -D analyzer-optimize
 -D js-es=6
 -D js_global=globalThis
--D NO_OMEMO
 -D no-traces
 -main test.TestAll
 -w -WDeprecated