Bug 1518172 - Import ctypes-otr, updated by kaie/aleca. r=florian,clokep,mkmelin
authorArlo Breault <arlolra@gmail.com>, Kai Engert <kaie@kuix.de>, Alessandro Castellani <alessandro@thunderbird.net>
Fri, 17 May 2019 16:21:48 +0200
changeset 26631 88f230af325e08d0a148c8c04fb691c92b58e600
parent 26630 4516cf2270f464e5e8bc19fbf2b0ca6fff10ca58
child 26632 7e3a1808d30d74cd63009b3a518d417566dd206d
push id15929
push userkaie@kuix.de
push dateFri, 17 May 2019 14:22:40 +0000
treeherdercomm-central@88f230af325e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, clokep, mkmelin
bugs1518172
Bug 1518172 - Import ctypes-otr, updated by kaie/aleca. r=florian,clokep,mkmelin Original MPL v2 code by Arlo Breault from https://github.com/arlolra/ctypes-otr/ Ported to Thunderbird by Kai Engert, includes UI changes by Alessandro Castellani.
chat/content/jar.mn
chat/content/otr-add-finger.dtd
chat/content/otr-add-finger.properties
chat/content/otr-add-fingerprint.js
chat/content/otr-add-fingerprint.xul
chat/content/otr-auth.dtd
chat/content/otr-auth.js
chat/content/otr-auth.properties
chat/content/otr-auth.xul
chat/content/otr-chat.dtd
chat/content/otr-generate-key.dtd
chat/content/otr-generate-key.js
chat/content/otr-generate-key.properties
chat/content/otr-generate-key.xul
chat/content/otr.properties
chat/content/otrUI.properties
chat/content/otrWorker.js
chat/locales/jar.mn
chat/modules/CLib.jsm
chat/modules/OTR.jsm
chat/modules/OTRHelpers.jsm
chat/modules/OTRLib.jsm
chat/modules/OTRUI.jsm
chat/modules/moz.build
chat/themes/icons/otr-connection-encrypted.svg
chat/themes/icons/otr-connection-finished.svg
chat/themes/jar.mn
chat/themes/otr.css
mail/app/profile/all-thunderbird.js
mail/components/im/content/chat-conversation-info.js
mail/components/im/content/chat-messenger.inc.xul
mail/components/im/content/chat-messenger.js
mail/components/im/themes/chat.css
--- a/chat/content/jar.mn
+++ b/chat/content/jar.mn
@@ -7,8 +7,24 @@ chat.jar:
 	content/chat/accounts.css
 	content/chat/browserRequest.js
 	content/chat/browserRequest.xul
 	content/chat/imAccountOptionsHelper.js
 	content/chat/chat-account-richlistitem.js
 *	content/chat/imtooltip.xml
 	content/chat/conversation-browser.js
 	content/chat/conv.html
+	content/chat/otr-add-fingerprint.js
+	content/chat/otr-add-fingerprint.xul
+	content/chat/otr-auth.js
+	content/chat/otr-auth.xul
+	content/chat/otr-generate-key.js
+	content/chat/otr-generate-key.xul
+	content/chat/otrWorker.js
+	content/chat/otr-auth.dtd
+	content/chat/otr-auth.properties
+	content/chat/otr-add-finger.dtd
+	content/chat/otr-add-finger.properties
+	content/chat/otr.properties
+	content/chat/otr-generate-key.dtd
+	content/chat/otr-generate-key.properties
+	content/chat/otrUI.properties
+	content/chat/otr-chat.dtd
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-add-finger.dtd
@@ -0,0 +1,9 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY addFingerDialog.title "Add Fingerprint">
+<!ENTITY addFingerDialog.finger "Fingerprint">
+<!ENTITY addFingerDialog.cancel "Skip">
+<!-- LOCALIZATION NOTE (addFingerDialog.tooltip): do not translate OTR which is the name of an encryption protocol -->
+<!ENTITY addFingerDialog.tooltip "If you know the 40 hex char fingerprint of your contact's OTR private key, enter it now.">
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-add-finger.properties
@@ -0,0 +1,6 @@
+# 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 http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (addfinger.title): %S is the name of a chat contact person
+addfinger.title=Enter the fingerprint of the OTR key used by %S
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-add-fingerprint.js
@@ -0,0 +1,50 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const {
+  XPCOMUtils,
+  l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/content/otr-add-finger.properties")
+);
+
+var args = window.arguments[0].wrappedJSObject;
+
+var otrAddFinger = {
+  onload() {
+    document.title = _("addfinger.title", args.screenname);
+
+    document.addEventListener("dialogaccept", () => {
+      return this.add();
+    });
+  },
+
+  oninput(e) {
+    e.value = e.value.replace(/[^0-9a-fA-F]/gi, "");
+    document.documentElement.getButton("accept").disabled = (e.value.length != 40);
+  },
+
+  add(e) {
+    let hex = document.getElementById("finger").value;
+    let context = OTR.getContextFromRecipient(
+      args.account,
+      args.protocol,
+      args.screenname
+    );
+    let finger = OTR.addFingerprint(context, hex);
+    if (finger.isNull())
+      return;
+    try {
+      // Ignore the return, this is just a test.
+      OTR.getUIConvFromContext(context);
+    } catch (error) {
+      // We expect that a conversation may not have been started.
+      context = null;
+    }
+    OTR.setTrust(finger, true, context);
+  },
+};
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-add-fingerprint.xul
@@ -0,0 +1,27 @@
+<?xml version="1.0" ?>
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css" ?>
+
+<!DOCTYPE window SYSTEM "chrome://chat/content/otr-add-finger.dtd">
+
+<dialog id="otrAddFingerDialog"
+        windowtype="OTR:AddFinger"
+        onload="otrAddFinger.onload()"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="&addFingerDialog.title;"
+        buttons="accept,cancel"
+        buttonlabelcancel="&addFingerDialog.cancel;"
+        buttondisabledaccept="true">
+
+  <script type="application/javascript" src="chrome://chat/content/otr-add-fingerprint.js"/>
+  <vbox flex="1">
+    <label value="&addFingerDialog.tooltip;" control="name" flex="1"/>
+    <hbox id="fingerBox" align="baseline" flex="1">
+      <label value="&addFingerDialog.finger;" control="name"/>
+      <textbox id="finger" oninput="otrAddFinger.oninput(this)" flex="1"/>
+    </hbox>
+  </vbox>
+</dialog>
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-auth.dtd
@@ -0,0 +1,20 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY authDialog.title "Verify contact's identity">
+<!ENTITY authDialog.authenticate "Verify">
+<!ENTITY authDialog.help "Help">
+<!ENTITY authDialog.yes "Yes">
+<!ENTITY authDialog.no "No">
+<!ENTITY authDialog.verified "I have verified that this is in fact the correct fingerprint.">
+<!ENTITY authDialog.manualVerification "Manual fingerprint verification">
+<!ENTITY authDialog.questionAndAnswer "Question and answer">
+<!ENTITY authDialog.sharedSecret "Shared secret">
+<!ENTITY authDialog.manualInstruction "To verify the fingerprint, contact your intended chat partner via some other authenticated channel, such as the telephone or GPG-signed email. Each of you should tell your fingerprint to the other. If everything matches up, you should indicate in the dialog below that you have verified the fingerprint.">
+<!ENTITY authDialog.how "How would you like to verify your contact's identity?">
+<!ENTITY authDialog.qaInstruction "To verify their identity, pick a question whose answer is known only to you and your contact. Enter this question and answer, then wait for your contact to enter the answer as well. If the answers do not match, then you may be talking to an imposter.">
+<!ENTITY authDialog.secretInstruction "To verify their identity, pick a secret known only to you and your contact. Enter this secret, then wait for your contact to enter it as well. If the secrets do not match, then you may be talking to an imposter.">
+<!ENTITY authDialog.question "Enter question here:">
+<!ENTITY authDialog.answer "Enter secret answer here (case sensitive):">
+<!ENTITY authDialog.secret "Enter secret here:">
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-auth.js
@@ -0,0 +1,175 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+const {
+  XPCOMUtils,
+  l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/content/otr-auth.properties")
+);
+
+var [mode, uiConv, contactInfo] = window.arguments;
+
+// This window implements the interactive authentication of a buddy's
+// key. At open time, we're given several parameters, and the "mode"
+// parameter tells us, from where we've been called.
+// mode == "pref" means that we have been opened from the preferences,
+// and it means we cannot rely on the other user being online, and
+// we there might be no uiConv active currently, so we fall back.
+
+document.title = _("auth.title",
+  (mode === "pref") ? contactInfo.screenname : uiConv.normalizedName);
+
+function showSection(selected, hideMenu) {
+  document.getElementById("how").hidden = !!hideMenu;
+  [ "questionAndAnswer",
+    "sharedSecret",
+    "manualVerification",
+    "ask",
+  ].forEach(function(key) {
+    document.getElementById(key).hidden = (key !== selected);
+  });
+  window.sizeToContent();
+}
+
+function startSMP(context, answer, question) {
+  OTR.sendSecret(context, answer, question);
+  OTR.authUpdate(context, 10);
+}
+
+function manualVerification(fingerprint, context) {
+  let opts = document.getElementById("verifiedOption");
+  let trust = (opts.selectedItem.value === "yes");
+  OTR.setTrust(fingerprint, trust, context);
+}
+
+function populateFingers(context, theirs, trust) {
+  let fingers = document.getElementById("fingerprints");
+  let yours = OTR.privateKeyFingerprint(context.account, context.protocol);
+  if (!yours)
+    throw new Error("Fingerprint should already be generated.");
+  fingers.value =
+    _("auth.yourFingerprint", context.account, yours) + "\n\n" +
+    _("auth.theirFingerprint", context.username, theirs);
+  let opts = document.getElementById("verifiedOption");
+  let verified = trust ? "yes" : "no";
+  for (let item of opts.menupopup.childNodes) {
+    if (verified === item.value) {
+      opts.selectedItem = item;
+      break;
+    }
+  }
+}
+
+var otrAuth = {
+  onload() {
+    document.addEventListener("dialogaccept", () => {
+      return this.accept();
+    });
+
+    document.addEventListener("dialogcancel", () => {
+      return this.cancel();
+    });
+
+    document.addEventListener("dialoghelp", () => {
+      return this.help();
+    });
+
+    let context, theirs;
+    switch (mode) {
+      case "start":
+        context = OTR.getContext(uiConv.target);
+        theirs = OTR.hashToHuman(context.fingerprint);
+        populateFingers(context, theirs, context.trust);
+        showSection("questionAndAnswer");
+        break;
+      case "pref":
+        context = OTR.getContextFromRecipient(
+          contactInfo.account,
+          contactInfo.protocol,
+          contactInfo.screenname
+        );
+        theirs = contactInfo.fingerprint;
+        populateFingers(context, theirs, contactInfo.trust);
+        showSection("manualVerification", true);
+        this.oninput({ value: true });
+        break;
+      case "ask":
+        document.getElementById("askLabel").textContent = contactInfo.question ?
+          _("auth.question", contactInfo.question)
+          : _("auth.secret");
+        showSection("ask", true);
+        break;
+    }
+  },
+
+  accept() {
+    // uiConv may not be present in pref mode
+    let context = uiConv ? OTR.getContext(uiConv.target) : null;
+    if (mode === "pref") {
+      manualVerification(contactInfo.fpointer, context);
+    } else if (mode === "start") {
+      let how = document.getElementById("howOption");
+      switch (how.selectedItem.value) {
+      case "questionAndAnswer":
+        let question = document.getElementById("question").value;
+        let answer = document.getElementById("answer").value;
+        startSMP(context, answer, question);
+        break;
+      case "sharedSecret":
+        let secret = document.getElementById("secret").value;
+        startSMP(context, secret);
+        break;
+      case "manualVerification":
+        manualVerification(context.fingerprint, context);
+        break;
+      default:
+        throw new Error("Unreachable!");
+      }
+    } else if (mode === "ask") {
+      let response = document.getElementById("response").value;
+      OTR.sendResponse(context, response);
+      OTR.authUpdate(context, contactInfo.progress);
+    } else {
+      throw new Error("Unreachable!");
+    }
+    return true;
+  },
+
+  cancel() {
+    if (mode === "ask") {
+      let context = OTR.getContext(uiConv.target);
+      OTR.abortSMP(context);
+    }
+  },
+
+  oninput(e) {
+    document.documentElement.getButton("accept").disabled = !e.value;
+  },
+
+  how() {
+    let how = document.getElementById("howOption").selectedItem.value;
+    switch (how) {
+    case "questionAndAnswer":
+      this.oninput(document.getElementById("answer"));
+      break;
+    case "sharedSecret":
+      this.oninput(document.getElementById("secret"));
+      break;
+    case "manualVerification":
+      this.oninput({ value: true });
+      break;
+    }
+    showSection(how);
+  },
+
+  help() {
+    Services.prompt.alert(window, _("auth.helpTitle"), _("auth.help"));
+  },
+
+};
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-auth.properties
@@ -0,0 +1,15 @@
+# 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 http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (auth.title): %S is the screen name of a chat contact person
+auth.title=Verify the identity of %S
+# LOCALIZATION NOTE (auth.yourFingerprint): 1st %S is the user's own screen name. 2nd %S is the fingerprint (a checksum) of the user's own encryption key.
+auth.yourFingerprint=Fingerprint for you, %S:\n%S
+# LOCALIZATION NOTE (auth.theirFingerprint): 1st %S is the screen name of a chat contact. 2nd %S is the fingerprint (a checksum) of the chat contact's encryption key.
+auth.theirFingerprint=Purported fingerprint for %S:\n%S
+auth.help=Verifying a contact's identity helps ensure that the person you are talking to is who they claim to be.
+auth.helpTitle=Verification help
+# LOCALIZATION NOTE (auth.question): %S is a question (any text is possible) that was received from a chat contact
+auth.question=This is the question asked by your contact:\n\n%S\n\nEnter secret answer here (case sensitive):
+auth.secret=Enter secret here:
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-auth.xul
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css" ?>
+
+<!DOCTYPE window SYSTEM "chrome://chat/content/otr-auth.dtd">
+
+<dialog
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+  xmlns:html="http://www.w3.org/1999/xhtml"
+  id="otrAuthDialog"
+  windowtype="OTR:Auth"
+  title="&authDialog.title;"
+  onload="otrAuth.onload()"
+  buttons="accept,cancel,help"
+  buttonlabelaccept="&authDialog.authenticate;"
+  buttonlabelhelp="&authDialog.help;"
+  buttondisabledaccept="true">
+
+  <script type="application/javascript" src="chrome://chat/content/otr-auth.js" />
+
+  <groupbox id="how" hidden="true">
+    <caption label="&authDialog.how;"/>
+    <menulist id="howOption" oncommand="otrAuth.how();">
+      <menupopup>
+        <menuitem label="&authDialog.questionAndAnswer;" value="questionAndAnswer" />
+        <menuitem label="&authDialog.sharedSecret;" value="sharedSecret" />
+        <menuitem label="&authDialog.manualVerification;" value="manualVerification" />
+      </menupopup>
+    </menulist>
+  </groupbox>
+
+  <groupbox id="questionAndAnswer" hidden="true">
+    <caption label="&authDialog.questionAndAnswer;" />
+    <description style="width: 300px; white-space: pre-wrap;">&authDialog.qaInstruction;</description>
+    <label value="&authDialog.question;" control="question" flex="1" />
+    <textbox id="question" />
+    <label value="&authDialog.answer;" control="answer" flex="1" />
+    <textbox id="answer" oninput="otrAuth.oninput(this)" />
+  </groupbox>
+
+  <groupbox id="sharedSecret" hidden="true">
+    <caption label="&authDialog.sharedSecret;" />
+    <description style="width: 300px; white-space: pre-wrap;">&authDialog.secretInstruction;</description>
+    <label value="&authDialog.secret;" control="secret" flex="1" />
+    <textbox id="secret" oninput="otrAuth.oninput(this)" />
+  </groupbox>
+
+  <groupbox id="manualVerification" hidden="true">
+    <caption label="&authDialog.manualVerification;" />
+    <description style="width: 300px; white-space: pre-wrap;">&authDialog.manualInstruction;</description>
+    <html:textarea id="fingerprints" rows="5" readonly="true" />
+    <hbox align="center">
+      <label value="&authDialog.verified;" />
+      <menulist id="verifiedOption">
+        <menupopup>
+          <menuitem label="&authDialog.yes;" value="yes" />
+          <menuitem label="&authDialog.no;" value="no" />
+        </menupopup>
+      </menulist>
+    </hbox>
+  </groupbox>
+
+  <groupbox id="ask" hidden="true">
+    <description id="askLabel" style="width: 300px; white-space: pre-wrap;" />
+    <textbox id="response" oninput="otrAuth.oninput(this)" />
+  </groupbox>
+
+</dialog>
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-chat.dtd
@@ -0,0 +1,8 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY state.label        "Encryption Status:">
+<!ENTITY start.label        "Start private conversation">
+<!ENTITY end.label          "End private conversation">
+<!ENTITY auth.label         "Verify your contact's identity">
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-generate-key.dtd
@@ -0,0 +1,6 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<!ENTITY privDialog.title "Generating private key">
+<!ENTITY privDialog.done "Done">
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-generate-key.js
@@ -0,0 +1,29 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const {
+  XPCOMUtils,
+  l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/content/otr-generate-key.properties")
+);
+
+var otrPriv = {
+
+  onload() {
+    let args = window.arguments[0].wrappedJSObject;
+    let priv = document.getElementById("priv");
+    priv.textContent = _("priv.account", args.account, OTR.protocolName(args.protocol));
+    OTR.generatePrivateKey(args.account, args.protocol).then(function() {
+      document.documentElement.getButton("accept").disabled = false;
+      document.documentElement.acceptDialog();
+    }).catch(function(err) {
+      document.documentElement.getButton("accept").disabled = false;
+      priv.textContent = _("priv.failed", String(err));
+    });
+  },
+};
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-generate-key.properties
@@ -0,0 +1,8 @@
+# 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 http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (priv.account): 1st %S is the name of the user's own chat account. 2nd %S is the chat communication protocol used by that account.
+priv.account=Generating private key for %S (%S) …
+# LOCALIZATION NOTE (priv.failed): %S contains an error message that describes the cause of the failure
+priv.failed=Generating key failed: %S
new file mode 100644
--- /dev/null
+++ b/chat/content/otr-generate-key.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0" ?>
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css" ?>
+
+<!DOCTYPE dialog [
+  <!ENTITY % chatPrivDTD SYSTEM "chrome://chat/content/otr-generate-key.dtd">
+  %chatPrivDTD;
+]>
+
+<dialog
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+  id="otrPrivDialog"
+  windowtype="OTR:Priv"
+  title="&privDialog.title;"
+  buttons="accept"
+  buttonlabelaccept="&privDialog.done;"
+  onload="otrPriv.onload()"
+  buttondisabledaccept="true">
+
+  <script type="application/javascript" src="chrome://chat/content/otr-generate-key.js" />
+  <description id="priv" style="width: 300px; white-space: pre-wrap;"></description>
+
+</dialog>
new file mode 100644
--- /dev/null
+++ b/chat/content/otr.properties
@@ -0,0 +1,72 @@
+# 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 http://mozilla.org/MPL/2.0/.
+
+# LOCALIZATION NOTE (msgevent.encryption_required_part1): %S is the name of a chat contact
+msgevent.encryption_required_part1=You attempted to send an unencrypted message to %S. As a policy, unencrypted messages are not allowed.
+msgevent.encryption_required_part2=Attempting to start a private conversation. Your message will be retransmitted when the private conversation starts.
+msgevent.encryption_error=An error occurred when encrypting your message. The message was not sent.
+
+# LOCALIZATION NOTE (msgevent.connection_ended): %S is the name of a chat contact
+msgevent.connection_ended=%S has already closed their private connection to you. Your message was not sent. Either end your private conversation, or restart it.
+
+# LOCALIZATION NOTE (msgevent.setup_error): %S is the name of a chat contact
+msgevent.setup_error=An error occured while setting up a private conversation with %S.
+# LOCALIZATION NOTE (msgevent.msg_reflected): do not translate OTR which is the name of an encryption protocol
+msgevent.msg_reflected=You are receiving your own OTR messages. You are either trying to talk to yourself, or someone is reflecting your messages back at you.
+
+# LOCALIZATION NOTE (msgevent.msg_resent): %S is the name of a chat contact
+msgevent.msg_resent=The last message to %S was resent.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_not_private): %S is the name of a chat contact
+msgevent.rcvdmsg_not_private=The encrypted message received from %S is unreadable, as you are not currently communicating privately.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_unreadable): %S is the name of a chat contact
+msgevent.rcvdmsg_unreadable=We received an unreadable encrypted message from %S.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_malformed): %S is the name of a chat contact
+msgevent.rcvdmsg_malformed=We received a malformed data message from %S.
+
+# LOCALIZATION NOTE (msgevent.log_heartbeat_rcvd): %S is the name of a chat contact. A Heartbeat is a technical message used to keep a connection alive.
+msgevent.log_heartbeat_rcvd=Heartbeat received from %S.
+
+# LOCALIZATION NOTE (msgevent.log_heartbeat_sent): %S is the name of a chat contact. A Heartbeat is a technical message used to keep a connection alive.
+msgevent.log_heartbeat_sent=Heartbeat sent to %S.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_general_err): do not translate OTR which is the name of an encryption protocol
+msgevent.rcvdmsg_general_err=An OTR error occured.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_unencrypted): 1st %S is the name of a chat contact. 2nd %S is the message that was received.
+msgevent.rcvdmsg_unencrypted=The following message received from %S was not encrypted: %S
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_unrecognized): do not translate OTR which is the name of an encryption protocol. %S is the name of a chat contact.
+msgevent.rcvdmsg_unrecognized=We received an unrecognized OTR message from %S.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_for_other_instance): %S is the name of a chat contact
+msgevent.rcvdmsg_for_other_instance=%S has sent a message intended for a different session. If you are logged in multiple times, another session may have received the message.
+
+# LOCALIZATION NOTE (context.gone_secure_private): %S is the name of a chat contact
+context.gone_secure_private=Private conversation with %S started.
+
+# LOCALIZATION NOTE (context.gone_secure_unverified): %S is the name of a chat contact
+context.gone_secure_unverified=Private conversation with %S started. However, their identity has not been verified.
+
+# LOCALIZATION NOTE (context.still_secure): %S is the name of a chat contact
+context.still_secure=Successfully refreshed the private conversation with %S.
+
+error.enc=Error occurred encrypting message.
+
+# LOCALIZATION NOTE (error.not_priv): %S is the name of a chat contact
+error.not_priv=You sent encrypted data to %S, who wasn't expecting it.
+error.unreadable=You transmitted an unreadable encrypted message.
+error.malformed=You transmitted a malformed data message.
+resent=[resent]
+# LOCALIZATION NOTE (tlv.disconnected): %S is the name of a chat contact
+tlv.disconnected=%S has ended their private conversation with you; you should do the same.
+# LOCALIZATION NOTE (query.msg): %S is the name of a chat contact. Do not translate "Off-the-Record" and "OTR" which is the name of an encryption protocol
+query.msg=%S has requested an Off-the-Record (OTR) private conversation. However, you do not have a plugin to support that. See https://en.wikipedia.org/wiki/Off-the-Record_Messaging for more information.
+trust.unused=Unused
+trust.not_private=Not Private
+trust.unverified=Unverified
+trust.private=Private
+trust.finished=Finished
new file mode 100644
--- /dev/null
+++ b/chat/content/otrUI.properties
@@ -0,0 +1,59 @@
+# 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 http://mozilla.org/MPL/2.0/.
+
+start.label=Start private conversation
+refresh.label=Refresh private conversation
+auth.label=Verify your contact's identity
+auth.cancel=Cancel
+auth.cancelAccessKey=C
+auth.error=An error occurred while verifying your contact's identity.
+auth.success=Verifying your contact's identity completed successfully.
+auth.successThem=Your contact has successfully verified your identity. You may want to verify their identity as well by asking your own question.
+auth.fail=Failed to verify your contact's identity.
+auth.waiting=Waiting for contact to complete verification …
+reauth.label=Reverify your contact's identity
+finger.verify=Verify
+verify.accessKey=V
+
+# LOCALIZATION NOTE (buddycontextmenu.label): do not translate OTR which is the name of an encryption protocol
+buddycontextmenu.label=Add Contact's OTR Fingerprint
+
+# LOCALIZATION NOTE (alert.start): %S is the name of a chat contact
+alert.start=Attempting to start a private conversation with %S.
+# LOCALIZATION NOTE (alert.refresh): %S is the name of a chat contact
+alert.refresh=Attempting to refresh the private conversation with %S.
+# LOCALIZATION NOTE (alert.gone_insecure): %S is the name of a chat contact
+alert.gone_insecure=Private conversation with %S ended.
+
+# LOCALIZATION NOTE (finger.unseen): %S is the name of a chat contact
+finger.unseen=The identity of %S has not been verified yet. Casual eavesdropping is not possible, but with some effort someone could be listening in. You should verify this contact's identity.
+
+state.not_private=The current conversation is not private.
+
+# LOCALIZATION NOTE (state.unverified): %S is the name of a chat contact
+state.unverified=The current conversation is private but the identity of %S has not been verified.
+
+# LOCALIZATION NOTE (state.private): %S is the name of a chat contact
+state.private=The current conversation is private and the identity of %S has been verified.
+
+# LOCALIZATION NOTE (state.finished): %S is the name of a chat contact
+state.finished=%S has ended their private conversation with you; you should do the same.
+
+state.not_private.label=Insecure
+state.unverified.label=Unverified
+state.private.label=Private
+state.finished.label=Finished
+
+# LOCALIZATION NOTE (afterauth.private): %S is the name of a chat contact
+afterauth.private=You have verified the identity of %S.
+
+# LOCALIZATION NOTE (afterauth.unverified): %S is the name of a chat contact
+afterauth.unverified=The identity of %S has not been verified.
+
+verify.title=Verify your contact's identity
+error.title=Error
+success.title=End to End Encryption
+successThem.title=Verify your contact's identity
+fail.title=Unable to verify
+waiting.title=Verification request sent
new file mode 100644
--- /dev/null
+++ b/chat/content/otrWorker.js
@@ -0,0 +1,54 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/chrome-worker, node */
+importScripts("resource://gre/modules/workers/require.js");
+var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+var Funcs = {};
+
+// Only what we need from libotr.js
+Funcs.generateKey = function(path, otrl_version, newkeySource) {
+  // eslint-disable-next-line no-eval
+  let newkey = eval(newkeySource);  // jshint ignore:line
+  let libotr = ctypes.open(path);
+
+  let abi = ctypes.default_abi;
+  let gcry_error_t = ctypes.unsigned_int;
+
+  // Initialize the OTR library. Pass the version of the API you are using.
+  let otrl_init = libotr.declare(
+    "otrl_init", abi, gcry_error_t,
+    ctypes.unsigned_int, ctypes.unsigned_int, ctypes.unsigned_int
+  );
+
+  // Do the private key generation calculation. You may call this from a
+  // background thread.  When it completes, call
+  // otrl_privkey_generate_finish from the _main_ thread.
+  let otrl_privkey_generate_calculate = libotr.declare(
+    "otrl_privkey_generate_calculate", abi, gcry_error_t,
+    ctypes.void_t.ptr
+  );
+
+  otrl_init.apply(libotr, otrl_version);
+  let err = otrl_privkey_generate_calculate(newkey);
+  libotr.close();
+  if (err)
+    throw new Error("otrl_privkey_generate_calculate (" + err + ")");
+};
+
+var worker = new PromiseWorker.AbstractWorker();
+
+worker.dispatch = function(method, args = []) {
+  return Funcs[method](...args);
+};
+
+worker.postMessage = function(res, ...args) {
+  self.postMessage(res, ...args);
+};
+
+worker.close = function() {
+  self.close();
+};
+
+self.addEventListener("message", msg => worker.handleMessage(msg));
--- a/chat/locales/jar.mn
+++ b/chat/locales/jar.mn
@@ -17,8 +17,16 @@
 	locale/@AB_CD@/chat/irc.properties	(%irc.properties)
 	locale/@AB_CD@/chat/logger.properties	(%logger.properties)
 	locale/@AB_CD@/chat/matrix.properties	(%matrix.properties)
 	locale/@AB_CD@/chat/skype.properties	(%skype.properties)
 	locale/@AB_CD@/chat/status.properties	(%status.properties)
 	locale/@AB_CD@/chat/twitter.properties	(%twitter.properties)
 	locale/@AB_CD@/chat/xmpp.properties	(%xmpp.properties)
 	locale/@AB_CD@/chat/yahoo.properties	(%yahoo.properties)
+#	locale/@AB_CD@/chat/otr-auth.dtd	(%otr-auth.dtd)
+#	locale/@AB_CD@/chat/otr-auth.properties	(%otr-auth.properties)
+#	locale/@AB_CD@/chat/otr-add-finger.dtd	(%otr-add-finger.dtd)
+#	locale/@AB_CD@/chat/otr-add-finger.properties	(%otr-add-finger.properties)
+#	locale/@AB_CD@/chat/otr.properties	(%otr.properties)
+#	locale/@AB_CD@/chat/otr-generate-key.dtd	(%otr-generate-key.dtd)
+#	locale/@AB_CD@/chat/otr-generate-key.properties	(%otr-generate-key.properties)
+#	locale/@AB_CD@/chat/otrUI.properties	(%otrUI.properties)
new file mode 100644
--- /dev/null
+++ b/chat/modules/CLib.jsm
@@ -0,0 +1,74 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+
+var OS = Services.appinfo.OS.toLowerCase();
+
+// type defs
+
+var FILE = ctypes.StructType("FILE");
+var fname_t = ctypes.char.ptr;
+var wchar_t = ctypes.char16_t;
+
+// Set the abi and path to CLib based on the OS.
+var libcAbi, libcPath;
+var strdup = "strdup";
+var fopen = "fopen";
+
+switch (OS) {
+case "win32":
+case "winnt":
+  libcAbi = ctypes.winapi_abi;
+  libcPath = ctypes.libraryName("msvcrt");
+  strdup = "_strdup";
+  fopen = "_wfopen";
+  fname_t = wchar_t.ptr;
+  break;
+case "darwin":
+  libcAbi = ctypes.default_abi;
+  libcPath = ctypes.libraryName("c");
+  break;
+case "linux":
+  libcAbi = ctypes.default_abi;
+  libcPath = "libc.so.6";
+  break;
+default:
+  throw new Error("Unknown OS");
+}
+
+var libc = ctypes.open(libcPath);
+
+var CLib = {
+  FILE,
+  memcmp: libc.declare(
+    "memcmp", libcAbi, ctypes.int,
+    ctypes.void_t.ptr,
+    ctypes.void_t.ptr,
+    ctypes.size_t
+  ),
+  free: libc.declare(
+    "free", libcAbi, ctypes.void_t,
+    ctypes.void_t.ptr
+  ),
+  strdup: libc.declare(
+    strdup, libcAbi, ctypes.char.ptr,
+    ctypes.char.ptr
+  ),
+  fclose: libc.declare(
+    "fclose", libcAbi, ctypes.int,
+    FILE.ptr
+  ),
+  fopen: libc.declare(
+    fopen, libcAbi, FILE.ptr,
+    fname_t,
+    fname_t
+  ),
+};
+
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["CLib"];
new file mode 100644
--- /dev/null
+++ b/chat/modules/OTR.jsm
@@ -0,0 +1,1110 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const {BasePromiseWorker} = ChromeUtils.import("resource://gre/modules/PromiseWorker.jsm");
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+const {
+  XPCOMUtils,
+  l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {CLib} = ChromeUtils.import("resource:///modules/CLib.jsm");
+const {OTRLib} = ChromeUtils.import("resource:///modules/OTRLib.jsm");
+var workerPath = "chrome://chat/content/otrWorker.js";
+const {OTRHelpers} = ChromeUtils.import("resource:///modules/OTRHelpers.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/content/otr.properties")
+);
+
+// some helpers
+
+function setInterval(fn, delay) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.init(fn, delay, Ci.nsITimer.TYPE_REPEATING_SLACK);
+  return timer;
+}
+
+function clearInterval(timer) {
+  timer.cancel();
+}
+
+// See: https://developer.mozilla.org/en-US/docs/Mozilla/js-ctypes/Using_js-ctypes/Working_with_data#Determining_if_two_pointers_are_equal
+function comparePointers(p, q) {
+  p = ctypes.cast(p, ctypes.uintptr_t).value.toString();
+  q = ctypes.cast(q, ctypes.uintptr_t).value.toString();
+  return p === q;
+}
+
+function trustFingerprint(fingerprint) {
+  return (!fingerprint.isNull() &&
+    !fingerprint.contents.trust.isNull() &&
+    fingerprint.contents.trust.readString().length > 0);
+}
+
+// Report whether you think the given user is online. Return 1 if you think
+// they are, 0 if you think they aren't, -1 if you're not sure.
+function isOnline(conv) {
+  let ret = -1;
+  if (conv.buddy)
+    ret = conv.buddy.online ? 1 : 0;
+  return ret;
+}
+
+// Use the protocol name in user facing strings. See trac #16490
+var names;
+function protocolName(aNormalizedName) {
+  if (!names) {
+    names = new Map();
+    let protocols = Services.core.getProtocols();
+    while (protocols.hasMoreElements()) {
+      let protocol = protocols.getNext();
+      names.set(protocol.normalizedName, protocol.name);
+    }
+  }
+  return names.get(aNormalizedName) || aNormalizedName;
+}
+
+
+// OTRLib context wrapper
+
+function Context(context) {
+  this._context = context;
+}
+
+Context.prototype = {
+  constructor: Context,
+  get username() { return this._context.contents.username.readString(); },
+  get account() { return this._context.contents.accountname.readString(); },
+  get protocol() { return this._context.contents.protocol.readString(); },
+  get msgstate() { return this._context.contents.msgstate; },
+  get fingerprint() { return this._context.contents.active_fingerprint; },
+  get trust() { return trustFingerprint(this.fingerprint); },
+};
+
+
+// otr module
+
+var OTR = {
+
+  hasRan: false,
+  libLoaded: false,
+  once() {
+    this.hasRan = true;
+    try {
+      if (OTRLib && OTRLib.init()) {
+        this.initUiOps();
+        OTR.libLoaded = true;
+      }
+    } catch (e) {
+      console.log(e);
+    }
+  },
+
+  privateKeyPath: OTRHelpers.profilePath("otr.private_key"),
+  fingerprintsPath: OTRHelpers.profilePath("otr.fingerprints"),
+  instanceTagsPath: OTRHelpers.profilePath("otr.instance_tags"),
+
+  init(opts) {
+    opts = opts || {};
+
+    if (!this.hasRan)
+      this.once();
+
+    if (!OTR.libLoaded)
+      return;
+
+    this.verifyNudge = !!opts.verifyNudge;
+    this.setPolicy(opts.requireEncryption);
+    this.userstate = OTRLib.otrl_userstate_create();
+
+    // A map of UIConvs, keyed on the target.id
+    this._convos = new Map();
+    this._observers = [];
+    this._buffer = [];
+    this._poll_timer = null;
+
+    // Async sending may fail in the transport protocols, so periodically
+    // drop old messages from the internal buffer. Should be rare.
+    const pluck_time = 1 * 60 * 1000;
+    this._pluck_timer = setInterval(() => {
+      let buf = this._buffer;
+      for (let i = 0; i < buf.length;) {
+        if ((Date.now() - buf[i].time) > pluck_time) {
+          this.log("dropping an old message: " + buf[i].display);
+          buf.splice(i, 1);
+        } else {
+          i += 1;
+        }
+      }
+    }, pluck_time);
+  },
+
+  close() {
+    if (this._poll_timer) {
+      clearInterval(this._poll_timer);
+      this._poll_timer = null;
+    }
+    if (this._pluck_timer) {
+      clearInterval(this._pluck_timer);
+      this._pluck_timer = null;
+    }
+    this._buffer = null;
+  },
+
+  log(msg) {
+    this.notifyObservers(msg, "otr:log");
+  },
+
+  protocolName,
+
+  setPolicy(requireEncryption) {
+    if (!OTR.libLoaded)
+      return;
+    this.policy = requireEncryption
+      ? OTRLib.OTRL_POLICY_ALWAYS
+      : OTRLib.OTRL_POLICY_OPPORTUNISTIC;
+  },
+
+  // load stored files from my profile
+  loadFiles() {
+    return Promise.all([
+      OS.File.exists(this.privateKeyPath).then((exists) => {
+        if (exists && OTRLib.otrl_privkey_read(
+          this.userstate, this.privateKeyPath
+        )) throw new Error("Failed to read private keys.");
+      }),
+      OS.File.exists(this.fingerprintsPath).then((exists) => {
+        if (exists && OTRLib.otrl_privkey_read_fingerprints(
+          this.userstate, this.fingerprintsPath, null, null
+        )) throw new Error("Failed to read fingerprints.");
+      }),
+      OS.File.exists(this.instanceTagsPath).then((exists) => {
+        if (exists && OTRLib.otrl_instag_read(
+          this.userstate, this.instanceTagsPath
+        )) throw new Error("Failed to read instance tags.");
+      }),
+    ]);
+  },
+
+  // generate a private key in a worker
+  generatePrivateKey(account, protocol) {
+    let newkey = new ctypes.void_t.ptr();
+    let err = OTRLib.otrl_privkey_generate_start(
+      OTR.userstate, account, protocol, newkey.address()
+    );
+    if (err || newkey.isNull())
+      return Promise.reject("otrl_privkey_generate_start (" + err + ")");
+    let worker = new BasePromiseWorker(workerPath);
+    return worker.post("generateKey", [
+      OTRLib.path, OTRLib.otrl_version, newkey.toSource(),
+    ]).then(function() {
+      let err = OTRLib.otrl_privkey_generate_finish(
+        OTR.userstate, newkey, OTR.privateKeyPath
+      );
+      if (err)
+        throw new Error("otrl_privkey_generate_calculate (" + err + ")");
+    }).catch(function(err) {
+      if (!newkey.isNull())
+        OTRLib.otrl_privkey_generate_cancelled(OTR.userstate, newkey);
+      throw err;
+    });
+  },
+
+  // write fingerprints to file synchronously
+  writeFingerprints() {
+    if (OTRLib.otrl_privkey_write_fingerprints(
+      this.userstate, this.fingerprintsPath
+    )) throw new Error("Failed to write fingerprints.");
+  },
+
+  // generate instance tag synchronously
+  generateInstanceTag(account, protocol) {
+    if (OTRLib.otrl_instag_generate(
+      this.userstate, this.instanceTagsPath, account, protocol
+    )) throw new Error("Failed to generate instance tag.");
+  },
+
+  // get my fingerprint
+  privateKeyFingerprint(account, protocol) {
+    let fingerprint = OTRLib.otrl_privkey_fingerprint(
+      this.userstate, new OTRLib.fingerprint_t(), account, protocol
+    );
+    return fingerprint.isNull() ? null : fingerprint.readString();
+  },
+
+  // return a human readable string for a fingerprint
+  hashToHuman(fingerprint) {
+    let hash = fingerprint.contents.fingerprint;
+    if (hash.isNull())
+      throw Error("No fingerprint found.");
+    let human = new OTRLib.fingerprint_t();
+    OTRLib.otrl_privkey_hash_to_human(human, hash);
+    return human.readString();
+  },
+
+  base64encode(data, dataLen) {
+    // CData objects are initialized with zeroes.  The plus one gives us
+    // our null byte so that readString below is safe.
+    let buf = ctypes.char.array(Math.floor((dataLen + 2) / 3) * 4 + 1)();
+    OTRLib.otrl_base64_encode(buf, data, dataLen); // ignore returned size
+    return buf.readString();  // str
+  },
+
+  base64decode(str) {
+    let size = str.length;
+    // +1 here so that we're safe in calling readString on data in the tests.
+    let data = ctypes.unsigned_char.array(Math.floor((size + 3) / 4) * 3 + 1)();
+    OTRLib.otrl_base64_decode(data, str, size); // ignore returned len
+    // We aren't returning the dataLen since we know the hash length in our
+    // one use case so far.
+    return data;
+  },
+
+  getTrustLevel(context) {
+    let level = OTR.trust(context);
+    if (level === OTR.trustState.TRUST_PRIVATE) {
+      return OTR.trustState.TRUST_PRIVATE;
+    } else if (level === OTR.trustState.TRUST_UNVERIFIED) {
+      return OTR.trustState.TRUST_UNVERIFIED;
+    } else if (level === OTR.trustState.TRUST_FINISHED) {
+      return OTR.trustState.TRUST_FINISHED;
+    }
+    return OTR.trustState.TRUST_NOT_PRIVATE;
+  },
+
+  getStatus(level) {
+    switch (level) {
+    case OTR.trustState.TRUST_NOT_PRIVATE:
+      return _("trust.not_private");
+    case OTR.trustState.TRUST_UNVERIFIED:
+      return _("trust.unverified");
+    case OTR.trustState.TRUST_PRIVATE:
+      return _("trust.private");
+    case OTR.trustState.TRUST_FINISHED:
+      return _("trust.finished");
+    }
+    throw new Error("unknown level");
+  },
+
+  // get list of known fingerprints
+  knownFingerprints() {
+    let fps = [];
+    for (
+      let context = this.userstate.contents.context_root;
+      !context.isNull();
+      context = context.contents.next
+    ) {
+      // skip child contexts
+      if (!comparePointers(context.contents.m_context, context))
+        continue;
+      let wContext = new Context(context);
+      for (
+        let fingerprint = context.contents.fingerprint_root.next;
+        !fingerprint.isNull();
+        fingerprint = fingerprint.contents.next
+      ) {
+        let trust = trustFingerprint(fingerprint);
+        let used = false;
+        let best_level = OTR.trustState.TRUST_NOT_PRIVATE;
+        for (
+          let context_itr = context;
+          !context_itr.isNull() &&
+            comparePointers(context_itr.contents.m_context, context);
+          context_itr = context_itr.contents.next
+        ) {
+          if (comparePointers(
+            context_itr.contents.active_fingerprint, fingerprint
+          )) {
+            used = true;
+            best_level = OTR.getTrustLevel(new Context(context_itr));
+          }
+        }
+        fps.push({
+          fpointer: fingerprint.contents.address(),
+          fingerprint: OTR.hashToHuman(fingerprint),
+          screenname: wContext.username,
+          account: wContext.account,
+          protocol: wContext.protocol,
+          trust,
+          status: used ? OTR.getStatus(best_level) : _("trust.unused"),
+          purge: false,
+        });
+      }
+    }
+    return fps;
+  },
+
+  forgetFingerprints(fps) {
+    let write = false;
+    fps.forEach(function(obj, i) {
+      if (!obj.purge)
+        return;
+      obj.purge = false;  // reset early
+      let fingerprint = obj.fpointer;
+      if (fingerprint.isNull())
+        return;
+      // don't do anything if fp is active and we're in an encrypted state
+      let context = fingerprint.contents.context.contents.m_context;
+      for (
+        let context_itr = context;
+        !context_itr.isNull() &&
+          comparePointers(context_itr.contents.m_context, context);
+        context_itr = context_itr.contents.next
+      ) {
+        if (
+          context_itr.contents.msgstate === OTRLib.messageState.OTRL_MSGSTATE_ENCRYPTED &&
+          comparePointers(context_itr.contents.active_fingerprint, fingerprint)
+        ) return;
+      }
+      write = true;
+      OTRLib.otrl_context_forget_fingerprint(fingerprint, 1);
+      fps[i] = null;  // null out removed fps
+    });
+    if (write)
+      OTR.writeFingerprints();
+  },
+
+  addFingerprint(context, hex) {
+    let fingerprint = new OTRLib.hash_t();
+    if (hex.length != 40) throw new Error("Invalid fingerprint value.");
+    let bytes = hex.match(/.{1,2}/g);
+    for (let i = 0; i < 20; i++)
+      fingerprint[i] = parseInt(bytes[i], 16);
+    return OTRLib.otrl_context_find_fingerprint(context._context, fingerprint, 1, null);
+  },
+
+  getFingerprintsForRecipient(account, protocol, recipient) {
+    let fingers = OTR.knownFingerprints();
+    return fingers.filter(function(fg) {
+      return fg.account == account &&
+             fg.protocol == protocol &&
+             fg.screenname == recipient;
+    });
+  },
+
+  isFingerprintTrusted(fingerprint) {
+    return !!OTRLib.otrl_context_is_fingerprint_trusted(fingerprint);
+  },
+
+  // update trust in fingerprint
+  setTrust(fingerprint, trust, context) {
+    // ignore if no change in trust
+    if (context && (trust === context.trust))
+      return;
+    OTRLib.otrl_context_set_trust(fingerprint, trust ? "verified" : "");
+    this.writeFingerprints();
+    if (context)
+      this.notifyTrust(context);
+  },
+
+  notifyTrust(context) {
+    this.notifyObservers(context, "otr:msg-state");
+    this.notifyObservers(context, "otr:trust-state");
+  },
+
+  authUpdate(context, progress, success) {
+    this.notifyObservers({
+      context,
+      progress,
+      success,
+    }, "otr:auth-update");
+  },
+
+  // expose message states
+  getMessageState() {
+    return OTRLib.messageState;
+  },
+
+  // get context from conv
+  getContext(conv) {
+    let context = OTRLib.otrl_context_find(
+      this.userstate,
+      conv.normalizedName,
+      conv.account.normalizedName,
+      // TODO: check why sometimes normalizedName is undefined, and if
+      // that's ok. Fallback wasn't necessary in the original code.
+      conv.account.protocol.normalizedName || "",
+      OTRLib.instag.OTRL_INSTAG_BEST, 1, null, null, null
+    );
+    return new Context(context);
+  },
+
+  getContextFromRecipient(account, protocol, recipient) {
+    let context = OTRLib.otrl_context_find(
+      this.userstate, recipient, account, protocol,
+      OTRLib.instag.OTRL_INSTAG_BEST, 1, null, null, null
+    );
+    return new Context(context);
+  },
+
+  getUIConvFromContext(context) {
+    return this.getUIConvForRecipient(
+      context.account, context.protocol, context.username
+    );
+  },
+
+  getUIConvForRecipient(account, protocol, recipient) {
+    let uiConvs = this._convos.values();
+    let uiConv = uiConvs.next();
+    while (!uiConv.done) {
+      let conv = uiConv.value.target;
+      if (conv.account.normalizedName === account &&
+          conv.account.protocol.normalizedName === protocol &&
+          conv.normalizedName === recipient) {
+          // console.log("=== getUIConvForRecipient found, account: " + account + "  protocol: " + protocol + "  recip: " + recipient);
+        return uiConv.value;
+      }
+      uiConv = uiConvs.next();
+    }
+    throw new Error("Couldn't find conversation.");
+  },
+
+  getUIConvFromConv(conv) {
+    // return this._convos.get(conv.id);
+    return Services.conversations.getUIConversation(conv);
+  },
+
+  disconnect(conv, remove) {
+    OTRLib.otrl_message_disconnect(
+      this.userstate,
+      this.uiOps.address(),
+      null,
+      conv.account.normalizedName,
+      conv.account.protocol.normalizedName,
+      conv.normalizedName,
+      OTRLib.instag.OTRL_INSTAG_BEST
+    );
+    if (remove) {
+      let uiConv = this.getUIConvFromConv(conv);
+      if (uiConv)
+        this.removeConversation(uiConv);
+    } else
+      this.notifyObservers(this.getContext(conv), "otr:disconnected");
+  },
+
+  sendQueryMsg(conv) {
+    let query = OTRLib.otrl_proto_default_query_msg(
+      conv.account.normalizedName,
+      this.policy
+    );
+    if (query.isNull()) {
+      Cu.reportError(new Error("Sending query message failed."));
+      return;
+    }
+    // Use the default msg to format the version.
+    // We don't supprt v1 of the protocol so this should be fine.
+    let queryMsg = /^\?OTR.*?\?/.exec(query.readString())[0] + "\n";
+    queryMsg += _("query.msg", conv.account.normalizedName);
+    conv.sendMsg(queryMsg);
+    OTRLib.otrl_message_free(query);
+  },
+
+  trustState: {
+    TRUST_NOT_PRIVATE: 0,
+    TRUST_UNVERIFIED: 1,
+    TRUST_PRIVATE: 2,
+    TRUST_FINISHED: 3,
+  },
+
+  // Check the attributes of the OTR context, and derive how that maps
+  // to one of the above trust states, which we'll show to the user.
+  // If we have an encrypted channel, it depends on the presence of a
+  // context.trust object, if we treat is as private or unverified.
+  trust(context) {
+    let level = this.trustState.TRUST_NOT_PRIVATE;
+    switch (context.msgstate) {
+      case OTRLib.messageState.OTRL_MSGSTATE_ENCRYPTED:
+        level = context.trust
+          ? this.trustState.TRUST_PRIVATE
+          : this.trustState.TRUST_UNVERIFIED;
+        break;
+      case OTRLib.messageState.OTRL_MSGSTATE_FINISHED:
+        level = this.trustState.TRUST_FINISHED;
+        break;
+    }
+    return level;
+  },
+
+  // uiOps callbacks
+
+  // Return the OTR policy for the given context.
+  policy_cb(opdata, context) {
+    return this.policy;
+  },
+
+  // Create a private key for the given accountname/protocol if desired.
+  create_privkey_cb(opdata, accountname, protocol) {
+    let args = {
+      account: accountname.readString(),
+      protocol: protocol.readString(),
+    };
+    this.notifyObservers(args, "otr:generate");
+  },
+
+  // Report whether you think the given user is online. Return 1 if you think
+  // they are, 0 if you think they aren't, -1 if you're not sure.
+  is_logged_in_cb(opdata, accountname, protocol, recipient) {
+    let conv = this.getUIConvForRecipient(
+      accountname.readString(),
+      protocol.readString(),
+      recipient.readString()
+    ).target;
+    return isOnline(conv);
+  },
+
+  // Send the given IM to the given recipient from the given
+  // accountname/protocol.
+  inject_message_cb(opdata, accountname, protocol, recipient, message) {
+    let aMsg = message.readString();
+    this.log("inject_message_cb (msglen:" + aMsg.length + "): " + aMsg);
+    this.getUIConvForRecipient(
+      accountname.readString(),
+      protocol.readString(),
+      recipient.readString()
+    ).target.sendMsg(aMsg);
+  },
+
+  // A new fingerprint for the given user has been received.
+  new_fingerprint_cb(opdata, us, accountname, protocol, username, fingerprint) {
+    let context = OTRLib.otrl_context_find(
+      us, username, accountname, protocol,
+      OTRLib.instag.OTRL_INSTAG_MASTER, 1, null, null, null
+    );
+
+    let seen = false;
+    let fp = context.contents.fingerprint_root.next;
+    while (!fp.isNull()) {
+      if (CLib.memcmp(fingerprint, fp.contents.fingerprint, new ctypes.size_t(20))) {
+        seen = true;
+        break;
+      }
+      fp = fp.contents.next;
+    }
+
+    // Only nudge on new fingerprint, as opposed to always.
+    if (!this.verifyNudge)
+      this.notifyObservers(new Context(context), "otr:unverified",
+        (seen ? "seen" : "unseen"));
+  },
+
+  // The list of known fingerprints has changed.  Write them to disk.
+  write_fingerprint_cb(opdata) {
+    this.writeFingerprints();
+  },
+
+  // A ConnContext has entered a secure state.
+  gone_secure_cb(opdata, context) {
+    context = new Context(context);
+    let str = "context.gone_secure_" + (context.trust ? "private" : "unverified");
+    this.notifyObservers(context, "otr:msg-state");
+    this.sendAlert(context, _(str, context.username));
+    if (this.verifyNudge && !context.trust)
+      this.notifyObservers(context, "otr:unverified", "unseen");
+  },
+
+  // A ConnContext has left a secure state.
+  gone_insecure_cb(opdata, context) {
+    // This isn't used. See: https://bugs.otr.im/lib/libotr/issues/48
+  },
+
+  // We have completed an authentication, using the D-H keys we already knew.
+  // is_reply indicates whether we initiated the AKE.
+  still_secure_cb(opdata, context, is_reply) {
+    // Indicate the private conversation was refreshed.
+    if (!is_reply) {
+      context = new Context(context);
+      this.notifyObservers(context, "otr:msg-state");
+      this.sendAlert(context, _("context.still_secure", context.username));
+    }
+  },
+
+  // Find the maximum message size supported by this protocol.
+  max_message_size_cb(opdata, context) {
+    context = new Context(context);
+    // These values are, for the most part, from pidgin-otr's mms_table.
+    switch (context.protocol) {
+    case "irc":
+    case "prpl-irc":
+      return 417;
+    case "facebook":
+    case "gtalk":
+    case "odnoklassniki":
+    case "jabber":
+    case "xmpp":
+      return 65536;
+    case "prpl-yahoo":
+      return 799;
+    case "prpl-msn":
+      return 1409;
+    case "prpl-icq":
+      return 2346;
+    case "prpl-gg":
+      return 1999;
+    case "prpl-aim":
+    case "prpl-oscar":
+      return 2343;
+    case "prpl-novell":
+      return 1792;
+    default:
+      return 0;
+    }
+  },
+
+  // We received a request from the buddy to use the current "extra" symmetric
+  // key.
+  received_symkey_cb(opdata, context, use, usedata, usedatalen, symkey) {
+    // Ignore until we have a use.
+  },
+
+  // Return a string according to the error event.
+  otr_error_message_cb(opdata, context, err_code) {
+    context = new Context(context);
+    let msg;
+    switch (err_code) {
+    case OTRLib.errorCode.OTRL_ERRCODE_ENCRYPTION_ERROR:
+      msg = _("error.enc");
+      break;
+    case OTRLib.errorCode.OTRL_ERRCODE_MSG_NOT_IN_PRIVATE:
+      msg = _("error.not_priv", context.username);
+      break;
+    case OTRLib.errorCode.OTRL_ERRCODE_MSG_UNREADABLE:
+      msg = _("error.unreadable");
+      break;
+    case OTRLib.errorCode.OTRL_ERRCODE_MSG_MALFORMED:
+      msg = _("error.malformed");
+      break;
+    default:
+      return null;
+    }
+    return CLib.strdup(msg);
+  },
+
+  // Deallocate a string returned by otr_error_message_cb.
+  otr_error_message_free_cb(opdata, err_msg) {
+    if (!err_msg.isNull())
+      CLib.free(err_msg);
+  },
+
+  // Return a string that will be prefixed to any resent message.
+  resent_msg_prefix_cb(opdata, context) {
+    return CLib.strdup(_("resent"));
+  },
+
+  // Deallocate a string returned by resent_msg_prefix.
+  resent_msg_prefix_free_cb(opdata, prefix) {
+    if (!prefix.isNull())
+      CLib.free(prefix);
+  },
+
+  // Update the authentication UI with respect to SMP events.
+  handle_smp_event_cb(opdata, smp_event, context, progress_percent, question) {
+    context = new Context(context);
+    switch (smp_event) {
+    case OTRLib.smpEvent.OTRL_SMPEVENT_NONE:
+      break;
+    case OTRLib.smpEvent.OTRL_SMPEVENT_ASK_FOR_ANSWER:
+    case OTRLib.smpEvent.OTRL_SMPEVENT_ASK_FOR_SECRET:
+      this.notifyObservers({
+        context,
+        progress: progress_percent,
+        question: question.isNull() ? null : question.readString(),
+      }, "otr:auth-ask");
+      break;
+    case OTRLib.smpEvent.OTRL_SMPEVENT_CHEATED:
+      OTR.abortSMP(context);
+      /* falls through */
+    case OTRLib.smpEvent.OTRL_SMPEVENT_IN_PROGRESS:
+    case OTRLib.smpEvent.OTRL_SMPEVENT_SUCCESS:
+    case OTRLib.smpEvent.OTRL_SMPEVENT_FAILURE:
+    case OTRLib.smpEvent.OTRL_SMPEVENT_ABORT:
+      this.authUpdate(context, progress_percent,
+        (smp_event === OTRLib.smpEvent.OTRL_SMPEVENT_SUCCESS));
+      break;
+    case OTRLib.smpEvent.OTRL_SMPEVENT_ERROR:
+      OTR.abortSMP(context);
+      break;
+    default:
+      this.log("smp event: " + smp_event);
+    }
+  },
+
+  // Handle and send the appropriate message(s) to the sender/recipient
+  // depending on the message events.
+  handle_msg_event_cb(opdata, msg_event, context, message, err) {
+    context = new Context(context);
+    switch (msg_event) {
+    case OTRLib.messageEvent.OTRL_MSGEVENT_NONE:
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_ENCRYPTION_REQUIRED:
+      this.sendAlert(context, _("msgevent.encryption_required_part1", context.username));
+      this.sendAlert(context, _("msgevent.encryption_required_part2"));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_ENCRYPTION_ERROR:
+      this.sendAlert(context, _("msgevent.encryption_error"));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_CONNECTION_ENDED:
+      this.sendAlert(context, _("msgevent.connection_ended", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_SETUP_ERROR:
+      this.sendAlert(context, _("msgevent.setup_error", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_MSG_REFLECTED:
+      this.sendAlert(context, _("msgevent.msg_reflected"));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_MSG_RESENT:
+      this.sendAlert(context, _("msgevent.msg_resent", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_NOT_IN_PRIVATE:
+      this.sendAlert(context, _("msgevent.rcvdmsg_not_private", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNREADABLE:
+      this.sendAlert(context, _("msgevent.rcvdmsg_unreadable", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_MALFORMED:
+      this.sendAlert(context, _("msgevent.rcvdmsg_malformed", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_LOG_HEARTBEAT_RCVD:
+      this.log(_("msgevent.log_heartbeat_rcvd", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_LOG_HEARTBEAT_SENT:
+      this.log(_("msgevent.log_heartbeat_sent", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_GENERAL_ERR:
+      this.sendAlert(context, _("msgevent.rcvdmsg_general_err"));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNENCRYPTED:
+      this.sendAlert(context, _("msgevent.rcvdmsg_unencrypted", context.username, message.isNull() ? "" : message.readString()));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNRECOGNIZED:
+      this.sendAlert(context, _("msgevent.rcvdmsg_unrecognized", context.username));
+      break;
+    case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_FOR_OTHER_INSTANCE:
+      this.log(_("msgevent.rcvdmsg_for_other_instance", context.username));
+      break;
+    default:
+      this.log("msg event: " + msg_event);
+    }
+  },
+
+  // Create an instance tag for the given accountname/protocol if desired.
+  create_instag_cb(opdata, accountname, protocol) {
+    this.generateInstanceTag(accountname.readString(), protocol.readString());
+  },
+
+  // When timer_control is called, turn off any existing periodic timer.
+  // Additionally, if interval > 0, set a new periodic timer to go off every
+  // interval seconds.
+  timer_control_cb(opdata, interval) {
+    if (this._poll_timer) {
+      clearInterval(this._poll_timer);
+      this._poll_timer = null;
+    }
+    if (interval > 0) {
+      this._poll_timer = setInterval(() => {
+        OTRLib.otrl_message_poll(this.userstate, this.uiOps.address(), null);
+      }, interval * 1000);
+    }
+  },
+
+  // uiOps
+
+  initUiOps() {
+    this.uiOps = new OTRLib.OtrlMessageAppOps();
+
+    let methods = [
+      "policy",
+      "create_privkey",
+      "is_logged_in",
+      "inject_message",
+      "update_context_list",  // not implemented
+      "new_fingerprint",
+      "write_fingerprint",
+      "gone_secure",
+      "gone_insecure",
+      "still_secure",
+      "max_message_size",
+      "account_name",  // not implemented
+      "account_name_free",  // not implemented
+      "received_symkey",
+      "otr_error_message",
+      "otr_error_message_free",
+      "resent_msg_prefix",
+      "resent_msg_prefix_free",
+      "handle_smp_event",
+      "handle_msg_event",
+      "create_instag",
+      "convert_msg",  // not implemented
+      "convert_free",  // not implemented
+      "timer_control",
+    ];
+
+    for (let i = 0; i < methods.length; i++) {
+      let m = methods[i];
+      if (!this[m + "_cb"]) {
+        this.uiOps[m] = null;
+        continue;
+      }
+      // keep a pointer to this in memory to avoid crashing
+      this[m + "_cb"] = OTRLib[m + "_cb_t"](this[m + "_cb"].bind(this));
+      this.uiOps[m] = this[m + "_cb"];
+    }
+  },
+
+  sendAlert(context, msg) {
+    this.getUIConvFromContext(context).systemMessage(msg);
+  },
+
+  observe(aObject, aTopic, aMsg) {
+    switch (aTopic) {
+    case "sending-message":
+      this.onSend(aObject);
+      break;
+    case "received-message":
+      this.onReceive(aObject);
+      break;
+    case "new-ui-conversation":
+      this.addConversation(aObject);
+      break;
+    }
+  },
+
+  addConversation(uiConv) {
+    let conv = uiConv.target;
+    if (conv.isChat)
+      return;
+    this._convos.set(conv.id, uiConv);
+    uiConv.addObserver(this);
+  },
+
+  removeConversation(uiConv) {
+    uiConv.removeObserver(this);
+    this._convos.delete(uiConv.target.id);
+    this.clearMsgs(uiConv.target.id);
+  },
+
+  sendSecret(context, secret, question) {
+    let str = ctypes.char.array()(secret);
+    let strlen = new ctypes.size_t(str.length - 1);
+    OTRLib.otrl_message_initiate_smp_q(
+      this.userstate,
+      this.uiOps.address(),
+      null,
+      context._context,
+      question ? question : null,
+      str,
+      strlen
+    );
+  },
+
+  sendResponse(context, response) {
+    let str = ctypes.char.array()(response);
+    let strlen = new ctypes.size_t(str.length - 1);
+    OTRLib.otrl_message_respond_smp(
+      this.userstate,
+      this.uiOps.address(),
+      null,
+      context._context,
+      str,
+      strlen
+    );
+  },
+
+  abortSMP(context) {
+    OTRLib.otrl_message_abort_smp(
+      this.userstate,
+      this.uiOps.address(),
+      null,
+      context._context
+    );
+  },
+
+  onSend(om) {
+    if (om.cancelled)
+      return;
+
+    let conv = om.conversation;
+    if (conv.isChat)
+      return;
+
+    // check for irc action messages
+    if (om.action) {
+      om.cancelled = true;
+      let uiConv = this.getUIConvFromConv(conv);
+      if (uiConv)
+        uiConv.sendMsg("/me " + om.message);
+      return;
+    }
+
+    let newMessage = new ctypes.char.ptr();
+
+    this.log("pre sending: " + om.message);
+
+    let err = OTRLib.otrl_message_sending(
+      this.userstate,
+      this.uiOps.address(),
+      null,
+      conv.account.normalizedName,
+      conv.account.protocol.normalizedName,
+      conv.normalizedName,
+      OTRLib.instag.OTRL_INSTAG_BEST,
+      om.message,
+      null,
+      newMessage.address(),
+      OTRLib.fragPolicy.OTRL_FRAGMENT_SEND_ALL_BUT_LAST,
+      null,
+      null,
+      null
+    );
+
+    let msg = om.message;
+
+    if (err) {
+      om.cancelled = true;
+      Cu.reportError(new Error("Failed to send message. Returned code: " + err));
+    } else if (!newMessage.isNull()) {
+      msg = newMessage.readString();
+      // https://bugs.otr.im/lib/libotr/issues/52
+      if (!msg) {
+        om.cancelled = true;
+      }
+    }
+
+    if (!om.cancelled) {
+      // OTR handshakes only work while both peers are online.
+      // Sometimes we want to include a special whitespace suffix,
+      // which the OTR protocol uses to signal that the sender is willing
+      // to start an OTR session. Don't do that for offline messages.
+      // See: https://bugs.otr.im/lib/libotr/issues/102
+      if (isOnline(conv) === 0 ||
+          // Twitter trims tweets.
+          conv.account.protocol.normalizedName === "twitter") {
+        let ind = msg.indexOf(OTRLib.OTRL_MESSAGE_TAG_BASE);
+        if (ind > -1) {
+          msg = msg.substring(0, ind);
+          let context = this.getContext(conv);
+          context._context.contents.otr_offer = OTRLib.otr_offer.OFFER_NOT;
+        }
+      }
+
+      this.bufferMsg(conv.id, om.message, msg);
+      om.message = msg;
+    }
+
+    this.log("post sending (" + !om.cancelled + "): " + om.message);
+    OTRLib.otrl_message_free(newMessage);
+  },
+
+  onReceive(im) {
+    if (im.cancelled || im.system)
+      return;
+
+    let conv = im.conversation;
+    if (conv.isChat)
+      return;
+
+    if (im.outgoing) {
+      this.log("outgoing message to display: " + im.displayMessage);
+      this.pluckMsg(im);
+      return;
+    }
+
+    let newMessage = new ctypes.char.ptr();
+    let tlvs = new OTRLib.OtrlTLV.ptr();
+
+    this.log("pre receiving: " + im.displayMessage);
+
+    let err = OTRLib.otrl_message_receiving(
+      this.userstate,
+      this.uiOps.address(),
+      null,
+      conv.account.normalizedName,
+      conv.account.protocol.normalizedName,
+      conv.normalizedName,
+      im.displayMessage,
+      newMessage.address(),
+      tlvs.address(),
+      null,
+      null,
+      null
+    );
+
+    if (!newMessage.isNull()) {
+      im.displayMessage = newMessage.readString();
+    }
+
+    // search tlvs for a disconnect msg
+    // https://bugs.otr.im/lib/libotr/issues/54
+    let tlv = OTRLib.otrl_tlv_find(tlvs, OTRLib.tlvs.OTRL_TLV_DISCONNECTED);
+    if (!tlv.isNull()) {
+      let context = this.getContext(conv);
+      this.notifyObservers(context, "otr:disconnected");
+      this.sendAlert(context, _("tlv.disconnected", conv.normalizedName));
+    }
+
+    if (err) {
+      this.log("error (" + err + ") ignoring: " + im.displayMessage);
+      im.cancelled = true;  // ignore
+    } else {
+      this.log("post receiving: " + im.displayMessage);
+    }
+
+    OTRLib.otrl_tlv_free(tlvs);
+    OTRLib.otrl_message_free(newMessage);
+  },
+
+  // observer interface
+
+  addObserver(observer) {
+    if (!this._observers.includes(observer))
+      this._observers.push(observer);
+  },
+
+  removeObserver(observer) {
+    this._observers = this._observers.filter(o => o !== observer);
+  },
+
+  notifyObservers(aSubject, aTopic, aData) {
+    for (let observer of this._observers) {
+      observer.observe(aSubject, aTopic, aData);
+    }
+  },
+
+  // buffer messages
+
+  clearMsgs(convId) {
+    this._buffer = this._buffer.filter((msg) => msg.convId !== convId);
+  },
+
+  bufferMsg(convId, display, sent) {
+    this._buffer.push({
+      convId,
+      display,
+      sent,
+      time: Date.now(),
+    });
+  },
+
+  pluckMsg(im) {
+    let buf = this._buffer;
+    for (let i = 0; i < buf.length; i++) {
+      let b = buf[i];
+      if (b.convId === im.conversation.id && b.sent === im.displayMessage) {
+        im.displayMessage = b.display;
+        buf.splice(i, 1);
+        this.log("displaying: " + b.display);
+        return;
+      }
+    }
+    // don't display if it wasn't buffered
+    im.cancelled = true;
+    this.log("not displaying: " + im.displayMessage);
+  },
+
+};
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["OTR"];
new file mode 100644
--- /dev/null
+++ b/chat/modules/OTRHelpers.jsm
@@ -0,0 +1,38 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+
+var OTRHelpers = {
+
+  profilePath(filename) {
+    return OS.Path.join(OS.Constants.Path.profileDir, filename);
+  },
+
+  * getAccounts() {
+    let accounts = Services.accounts.getAccounts();
+    while (accounts.hasMoreElements())
+      yield accounts.getNext();
+  },
+
+  readTextFile(filename) {
+    let decoder = new TextDecoder();
+    return OS.File.read(filename).then(function(array) {
+      return decoder.decode(array);
+    });
+  },
+
+  writeTextFile(filename, data) {
+    let encoder = new TextEncoder();
+    let array = encoder.encode(data);
+    // https://dutherenverseauborddelatable.wordpress.com/2014/02/05/is-my-data-on-the-disk-safety-properties-of-os-file-writeatomic/
+    return OS.File.writeAtomic(filename, array, { tmpPath: `${filename}.tmp` });
+  },
+
+};
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["OTRHelpers"];
new file mode 100644
--- /dev/null
+++ b/chat/modules/OTRLib.jsm
@@ -0,0 +1,918 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+const otrl_version = [4, 1, 1];
+
+const {CLib} = ChromeUtils.import("resource:///modules/CLib.jsm");
+const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var systemOS = Services.appinfo.OS.toLowerCase();
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+var abi = ctypes.default_abi;
+
+// Open libotr. Determine the path to the chrome directory and look for it
+// there first. If not, fallback to searching the standard locations.
+var libotr, libotrPath;
+
+function tryLoadOTR(name, suffix) {
+  let filename = ctypes.libraryName(name) + suffix;
+  let binPath = Services.dirsvc.get("XpcomLib", Ci.nsIFile).path;
+  let binDir = OS.Path.dirname(binPath);
+  libotrPath = OS.Path.join(binDir, filename);
+
+  try {
+    console.log("===> trying to load " + libotrPath);
+    libotr = ctypes.open(libotrPath);
+  } catch (e) {}
+
+  if (!libotr) {
+    try {
+      // look in standard locations
+      libotrPath = filename;
+      console.log("===> trying to load " +
+        libotrPath + " from system's standard locations");
+      libotr = ctypes.open(libotrPath);
+    } catch (e) {}
+  }
+}
+
+if (!libotr && (systemOS === "winnt" || systemOS === "darwin")) {
+  // otr.5.dll or otr.5.dylib
+  tryLoadOTR("otr.5", "");
+}
+
+if (!libotr && systemOS === "winnt") {
+  // otr-5.dll
+  tryLoadOTR("otr-5", "");
+}
+
+if (!libotr && systemOS === "winnt") {
+  // libotr-5.dll
+  tryLoadOTR("libotr-5", "");
+}
+
+if (!libotr && !(systemOS === "winnt") && !(systemOS === "darwin")) {
+  // libotr.so.5
+  tryLoadOTR("otr", ".5");
+}
+
+if (!libotr) {
+  tryLoadOTR("otr", "");
+}
+
+// Helper function to open files with the path properly encoded.
+var callWithFILEp = function() {
+  // Windows filenames are in UTF-16.
+  let charType = (systemOS === "winnt") ? "jschar" : "char";
+
+  let args = Array.from(arguments);
+  let func = args.shift() + "_FILEp";
+  let mode = ctypes[charType].array()(args.shift());
+  let ind = args.shift();
+  let filename = ctypes[charType].array()(args[ind]);
+
+  let file = CLib.fopen(filename, mode);
+  if (file.isNull())
+    return 1;
+
+  // Swap filename with file.
+  args[ind] = file;
+
+  let ret = OTRLib[func].apply(OTRLib, args);
+  CLib.fclose(file);
+  return ret;
+};
+
+// type defs
+
+const FILE = CLib.FILE;
+
+const time_t = ctypes.long;
+const gcry_error_t = ctypes.unsigned_int;
+const gcry_cipher_hd_t = ctypes.StructType("gcry_cipher_handle").ptr;
+const gcry_md_hd_t = ctypes.StructType("gcry_md_handle").ptr;
+const gcry_mpi_t = ctypes.StructType("gcry_mpi").ptr;
+
+const otrl_instag_t = ctypes.unsigned_int;
+const OtrlPolicy = ctypes.unsigned_int;
+const OtrlTLV = ctypes.StructType("s_OtrlTLV");
+const ConnContext = ctypes.StructType("context");
+const ConnContextPriv = ctypes.StructType("context_priv");
+const OtrlMessageAppOps = ctypes.StructType("s_OtrlMessageAppOps");
+const OtrlAuthInfo = ctypes.StructType("OtrlAuthInfo");
+const Fingerprint = ctypes.StructType("s_fingerprint");
+const s_OtrlUserState = ctypes.StructType("s_OtrlUserState");
+const OtrlUserState = s_OtrlUserState.ptr;
+const OtrlSMState = ctypes.StructType("OtrlSMState");
+const DH_keypair = ctypes.StructType("DH_keypair");
+const OtrlPrivKey = ctypes.StructType("s_OtrlPrivKey");
+const OtrlInsTag = ctypes.StructType("s_OtrlInsTag");
+const OtrlPendingPrivKey = ctypes.StructType("s_OtrlPendingPrivKey");
+
+const OTRL_PRIVKEY_FPRINT_HUMAN_LEN = 45;
+const fingerprint_t = ctypes.char.array(OTRL_PRIVKEY_FPRINT_HUMAN_LEN);
+const hash_t = ctypes.unsigned_char.array(20);
+
+const app_data_free_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr,
+]).ptr;
+
+// enums
+
+const OtrlErrorCode = ctypes.int;
+const OtrlSMPEvent = ctypes.int;
+const OtrlMessageEvent = ctypes.int;
+const OtrlFragmentPolicy = ctypes.int;
+const OtrlConvertType = ctypes.int;
+const OtrlMessageState = ctypes.int;
+const OtrlAuthState = ctypes.int;
+const OtrlSessionIdHalf = ctypes.int;
+const OtrlSMProgState = ctypes.int;
+const NextExpectedSMP = ctypes.int;
+
+// callback signatures
+
+const policy_cb_t = ctypes.FunctionType(abi, OtrlPolicy, [
+  ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const create_privkey_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const is_logged_in_cb_t = ctypes.FunctionType(abi, ctypes.int, [
+  ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const inject_message_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr, ctypes.char.ptr,
+  ctypes.char.ptr,
+]).ptr;
+
+const update_context_list_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr,
+]).ptr;
+
+const new_fingerprint_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, OtrlUserState, ctypes.char.ptr, ctypes.char.ptr,
+  ctypes.char.ptr, ctypes.unsigned_char.array(20),
+]).ptr;
+
+const write_fingerprint_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr,
+]).ptr;
+
+const gone_secure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const gone_insecure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const still_secure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ConnContext.ptr, ctypes.int,
+]).ptr;
+
+const max_message_size_cb_t = ctypes.FunctionType(abi, ctypes.int, [
+  ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const account_name_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
+  ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const account_name_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.char.ptr,
+]).ptr;
+
+const received_symkey_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ConnContext.ptr, ctypes.unsigned_int,
+  ctypes.unsigned_char.ptr, ctypes.size_t, ctypes.unsigned_char.ptr,
+]).ptr;
+
+const otr_error_message_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
+  ctypes.void_t.ptr, ConnContext.ptr, OtrlErrorCode,
+]).ptr;
+
+const otr_error_message_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.char.ptr,
+]).ptr;
+
+const resent_msg_prefix_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
+  ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const resent_msg_prefix_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.char.ptr,
+]).ptr;
+
+const handle_smp_event_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, OtrlSMPEvent, ConnContext.ptr, ctypes.unsigned_short,
+  ctypes.char.ptr,
+]).ptr;
+
+const handle_msg_event_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, OtrlMessageEvent, ConnContext.ptr, ctypes.char.ptr,
+  gcry_error_t,
+]).ptr;
+
+const create_instag_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const convert_msg_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ConnContext.ptr, OtrlConvertType, ctypes.char.ptr.ptr,
+  ctypes.char.ptr,
+]).ptr;
+
+const convert_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ConnContext.ptr, ctypes.char.ptr,
+]).ptr;
+
+const timer_control_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+  ctypes.void_t.ptr, ctypes.unsigned_int,
+]).ptr;
+
+// defines
+
+s_OtrlUserState.define([
+  { context_root: ConnContext.ptr },
+  { privkey_root: OtrlPrivKey.ptr },
+  { instag_root: OtrlInsTag.ptr },
+  { pending_root: OtrlPendingPrivKey.ptr },
+  { timer_running: ctypes.int },
+]);
+
+Fingerprint.define([
+  { next: Fingerprint.ptr },
+  { tous: Fingerprint.ptr.ptr },
+  { fingerprint: ctypes.unsigned_char.ptr },
+  { context: ConnContext.ptr },
+  { trust: ctypes.char.ptr },
+]);
+
+DH_keypair.define([
+  { groupid: ctypes.unsigned_int },
+  { priv: gcry_mpi_t },
+  { pub: gcry_mpi_t },
+]);
+
+OtrlSMState.define([
+  { secret: gcry_mpi_t },
+  { x2: gcry_mpi_t },
+  { x3: gcry_mpi_t },
+  { g1: gcry_mpi_t },
+  { g2: gcry_mpi_t },
+  { g3: gcry_mpi_t },
+  { g3o: gcry_mpi_t },
+  { p: gcry_mpi_t },
+  { q: gcry_mpi_t },
+  { pab: gcry_mpi_t },
+  { qab: gcry_mpi_t },
+  { nextExpected: NextExpectedSMP },
+  { received_question: ctypes.int },
+  { sm_prog_state: OtrlSMProgState },
+]);
+
+OtrlAuthInfo.define([
+  { authstate: OtrlAuthState },
+  { context: ConnContext.ptr },
+  { our_dh: DH_keypair },
+  { our_keyid: ctypes.unsigned_int },
+  { encgx: ctypes.unsigned_char.ptr },
+  { encgx_len: ctypes.size_t },
+  { r: ctypes.unsigned_char.array(16) },
+  { hashgx: ctypes.unsigned_char.array(32) },
+  { their_pub: gcry_mpi_t },
+  { their_keyid: ctypes.unsigned_int },
+  { enc_c: gcry_cipher_hd_t },
+  { enc_cp: gcry_cipher_hd_t },
+  { mac_m1: gcry_md_hd_t },
+  { mac_m1p: gcry_md_hd_t },
+  { mac_m2: gcry_md_hd_t },
+  { mac_m2p: gcry_md_hd_t },
+  { their_fingerprint: ctypes.unsigned_char.array(20) },
+  { initiated: ctypes.int },
+  { protocol_version: ctypes.unsigned_int },
+  { secure_session_id: ctypes.unsigned_char.array(20) },
+  { secure_session_id_len: ctypes.size_t },
+  { session_id_half: OtrlSessionIdHalf },
+  { lastauthmsg: ctypes.char.ptr },
+  { commit_sent_time: time_t },
+]);
+
+ConnContext.define([
+  { next: ConnContext.ptr },
+  { tous: ConnContext.ptr.ptr },
+  { context_priv: ConnContextPriv.ptr },
+  { username: ctypes.char.ptr },
+  { accountname: ctypes.char.ptr },
+  { protocol: ctypes.char.ptr },
+  { m_context: ConnContext.ptr },
+  { recent_rcvd_child: ConnContext.ptr },
+  { recent_sent_child: ConnContext.ptr },
+  { recent_child: ConnContext.ptr },
+  { our_instance: otrl_instag_t },
+  { their_instance: otrl_instag_t },
+  { msgstate: OtrlMessageState },
+  { auth: OtrlAuthInfo },
+  { fingerprint_root: Fingerprint },
+  { active_fingerprint: Fingerprint.ptr },
+  { sessionid: ctypes.unsigned_char.array(20) },
+  { sessionid_len: ctypes.size_t },
+  { sessionid_half: OtrlSessionIdHalf },
+  { protocol_version: ctypes.unsigned_int },
+  { otr_offer: ctypes.int },
+  { app_data: ctypes.void_t.ptr },
+  { app_data_free: app_data_free_t },
+  { smstate: OtrlSMState.ptr },
+]);
+
+OtrlMessageAppOps.define([
+  { policy: policy_cb_t },
+  { create_privkey: create_privkey_cb_t },
+  { is_logged_in: is_logged_in_cb_t },
+  { inject_message: inject_message_cb_t },
+  { update_context_list: update_context_list_cb_t },
+  { new_fingerprint: new_fingerprint_cb_t },
+  { write_fingerprint: write_fingerprint_cb_t },
+  { gone_secure: gone_secure_cb_t },
+  { gone_insecure: gone_insecure_cb_t },
+  { still_secure: still_secure_cb_t },
+  { max_message_size: max_message_size_cb_t },
+  { account_name: account_name_cb_t },
+  { account_name_free: account_name_free_cb_t },
+  { received_symkey: received_symkey_cb_t },
+  { otr_error_message: otr_error_message_cb_t },
+  { otr_error_message_free: otr_error_message_free_cb_t },
+  { resent_msg_prefix: resent_msg_prefix_cb_t },
+  { resent_msg_prefix_free: resent_msg_prefix_free_cb_t },
+  { handle_smp_event: handle_smp_event_cb_t },
+  { handle_msg_event: handle_msg_event_cb_t },
+  { create_instag: create_instag_cb_t },
+  { convert_msg: convert_msg_cb_t },
+  { convert_free: convert_free_cb_t },
+  { timer_control: timer_control_cb_t },
+]);
+
+OtrlTLV.define([
+  { type: ctypes.unsigned_short },
+  { len: ctypes.unsigned_short },
+  { data: ctypes.unsigned_char.ptr },
+  { next: OtrlTLV.ptr },
+]);
+
+// policies
+
+// const OTRL_POLICY_ALLOW_V1 = 0x01;
+const OTRL_POLICY_ALLOW_V2 = 0x02;
+
+// const OTRL_POLICY_ALLOW_V3 = 0x04;
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1550474 re v3.
+
+const OTRL_POLICY_REQUIRE_ENCRYPTION = 0x08;
+const OTRL_POLICY_SEND_WHITESPACE_TAG = 0x10;
+const OTRL_POLICY_WHITESPACE_START_AKE = 0x20;
+
+// const OTRL_POLICY_ERROR_START_AKE = 0x40;
+// Disabled to avoid automatic resend and MITM, as explained in
+// https://github.com/arlolra/ctypes-otr/issues/55
+
+var OTRLib;
+
+if (libotr) OTRLib = {
+
+  path: libotrPath,
+
+  // libotr API version
+  otrl_version,
+
+  init() {
+    // console.log("===> OTRLib.init()\n");
+    // apply version array as arguments to the init function
+    if (this.otrl_init.apply(this, this.otrl_version)) {
+      throw new Error("Couldn't initialize libotr.");
+    }
+    return true;
+  },
+
+  // proto.h
+
+  // If we ever see this sequence in a plaintext message, we'll assume the
+  // other side speaks OTR, and try to establish a connection.
+  OTRL_MESSAGE_TAG_BASE: " \t  \t\t\t\t \t \t \t  ",
+
+  OTRL_POLICY_OPPORTUNISTIC: new ctypes.unsigned_int(
+    OTRL_POLICY_ALLOW_V2 |
+    // OTRL_POLICY_ALLOW_V3 |
+    OTRL_POLICY_SEND_WHITESPACE_TAG |
+    OTRL_POLICY_WHITESPACE_START_AKE |
+    // OTRL_POLICY_ERROR_START_AKE |
+    0
+  ),
+
+  OTRL_POLICY_ALWAYS: new ctypes.unsigned_int(
+    OTRL_POLICY_ALLOW_V2 |
+    // OTRL_POLICY_ALLOW_V3 |
+    OTRL_POLICY_REQUIRE_ENCRYPTION |
+    OTRL_POLICY_WHITESPACE_START_AKE |
+    // OTRL_POLICY_ERROR_START_AKE |
+    0
+  ),
+
+  fragPolicy: {
+    OTRL_FRAGMENT_SEND_SKIP: 0,
+    OTRL_FRAGMENT_SEND_ALL: 1,
+    OTRL_FRAGMENT_SEND_ALL_BUT_FIRST: 2,
+    OTRL_FRAGMENT_SEND_ALL_BUT_LAST: 3,
+  },
+
+  // Return a pointer to a newly-allocated OTR query message, customized
+  // with our name.  The caller should free() the result when he's done
+  // with it.
+  otrl_proto_default_query_msg: libotr.declare(
+    "otrl_proto_default_query_msg", abi, ctypes.char.ptr,
+    ctypes.char.ptr, OtrlPolicy
+  ),
+
+  // Initialize the OTR library. Pass the version of the API you are using.
+  otrl_init: libotr.declare(
+    "otrl_init", abi, gcry_error_t,
+    ctypes.unsigned_int, ctypes.unsigned_int, ctypes.unsigned_int
+  ),
+
+  // instag.h
+
+  instag: {
+    OTRL_INSTAG_MASTER: new ctypes.unsigned_int(0),
+    OTRL_INSTAG_BEST: new ctypes.unsigned_int(1),
+    OTRL_INSTAG_RECENT: new ctypes.unsigned_int(2),
+    OTRL_INSTAG_RECENT_RECEIVED: new ctypes.unsigned_int(3),
+    OTRL_INSTAG_RECENT_SENT: new ctypes.unsigned_int(4),
+    OTRL_MIN_VALID_INSTAG: new ctypes.unsigned_int(0x100),
+  },
+
+  // Get a new instance tag for the given account and write to file.  The FILE*
+  // must be open for writing.
+  otrl_instag_generate: callWithFILEp.bind(null, "otrl_instag_generate", "wb", 1),
+  otrl_instag_generate_FILEp: libotr.declare(
+    "otrl_instag_generate_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr, ctypes.char.ptr, ctypes.char.ptr
+  ),
+
+  // Read our instance tag from a file on disk into the given OtrlUserState.
+  // The FILE* must be open for reading.
+  otrl_instag_read: callWithFILEp.bind(null, "otrl_instag_read", "rb", 1),
+  otrl_instag_read_FILEp: libotr.declare(
+    "otrl_instag_read_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr
+  ),
+
+  // Write our instance tags to a file on disk.  The FILE* must be open for
+  // writing.
+  otrl_instag_write: callWithFILEp.bind(null, "otrl_instag_write", "wb", 1),
+  otrl_instag_write_FILEp: libotr.declare(
+    "otrl_instag_write_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr
+  ),
+
+  // auth.h
+
+  authState: {
+    OTRL_AUTHSTATE_NONE: 0,
+    OTRL_AUTHSTATE_AWAITING_DHKEY: 1,
+    OTRL_AUTHSTATE_AWAITING_REVEALSIG: 2,
+    OTRL_AUTHSTATE_AWAITING_SIG: 3,
+    OTRL_AUTHSTATE_V1_SETUP: 4,
+  },
+
+  // b64.h
+
+  // base64 encode data.  Insert no linebreaks or whitespace.
+  // The buffer base64data must contain at least ((datalen+2)/3)*4 bytes of
+  // space. This function will return the number of bytes actually used.
+  otrl_base64_encode: libotr.declare(
+    "otrl_base64_encode", abi, ctypes.size_t,
+    ctypes.char.ptr, ctypes.unsigned_char.ptr, ctypes.size_t
+  ),
+
+  // base64 decode data.  Skip non-base64 chars, and terminate at the
+  // first '=', or the end of the buffer.
+  // The buffer data must contain at least ((base64len+3) / 4) * 3 bytes
+  // of space. This function will return the number of bytes actually
+  // used.
+  otrl_base64_decode: libotr.declare(
+    "otrl_base64_decode", abi, ctypes.size_t,
+    ctypes.unsigned_char.ptr, ctypes.char.ptr, ctypes.size_t
+  ),
+
+  // context.h
+
+  otr_offer: {
+    OFFER_NOT: 0,
+    OFFER_SENT: 1,
+    OFFER_REJECTED: 2,
+    OFFER_ACCEPTED: 3,
+  },
+
+  messageState: {
+    OTRL_MSGSTATE_PLAINTEXT: 0,
+    OTRL_MSGSTATE_ENCRYPTED: 1,
+    OTRL_MSGSTATE_FINISHED: 2,
+  },
+
+  // Look up a connection context by name/account/protocol/instance from the
+  // given OtrlUserState.
+  otrl_context_find: libotr.declare(
+    "otrl_context_find", abi, ConnContext.ptr,
+    OtrlUserState,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    otrl_instag_t,
+    ctypes.int,
+    ctypes.int.ptr,
+    ctypes.void_t.ptr,
+    ctypes.void_t.ptr
+  ),
+
+  // Set the trust level for a given fingerprint.
+  otrl_context_set_trust: libotr.declare(
+    "otrl_context_set_trust", abi, ctypes.void_t,
+    Fingerprint.ptr, ctypes.char.ptr
+  ),
+
+  // Find a fingerprint in a given context, perhaps adding it if not present.
+  otrl_context_find_fingerprint: libotr.declare(
+    "otrl_context_find_fingerprint", abi, Fingerprint.ptr,
+    ConnContext.ptr, hash_t, ctypes.int, ctypes.int.ptr
+  ),
+
+  // Forget a fingerprint (and maybe the whole context).
+  otrl_context_forget_fingerprint: libotr.declare(
+    "otrl_context_forget_fingerprint", abi, ctypes.void_t,
+    Fingerprint.ptr, ctypes.int
+  ),
+
+  // Return true iff the given fingerprint is marked as trusted.
+  otrl_context_is_fingerprint_trusted: libotr.declare(
+    "otrl_context_is_fingerprint_trusted", abi, ctypes.int,
+    Fingerprint.ptr
+  ),
+
+  // dh.h
+
+  sessionIdHalf: {
+    OTRL_SESSIONID_FIRST_HALF_BOLD: 0,
+    OTRL_SESSIONID_SECOND_HALF_BOLD: 1,
+  },
+
+  // sm.h
+
+  nextExpectedSMP: {
+    OTRL_SMP_EXPECT1: 0,
+    OTRL_SMP_EXPECT2: 1,
+    OTRL_SMP_EXPECT3: 2,
+    OTRL_SMP_EXPECT4: 3,
+    OTRL_SMP_EXPECT5: 4,
+  },
+
+  smProgState: {
+    OTRL_SMP_PROG_OK: 0,
+    OTRL_SMP_PROG_CHEATED: -2,
+    OTRL_SMP_PROG_FAILED: -1,
+    OTRL_SMP_PROG_SUCCEEDED: 1,
+  },
+
+  // userstate.h
+
+  // Create a new OtrlUserState.
+  otrl_userstate_create: libotr.declare(
+    "otrl_userstate_create", abi, OtrlUserState
+  ),
+
+  // privkey.h
+
+  // Generate a private DSA key for a given account, storing it into a file on
+  // disk, and loading it into the given OtrlUserState. Overwrite any
+  // previously generated keys for that account in that OtrlUserState.
+  otrl_privkey_generate: callWithFILEp.bind(null, "otrl_privkey_generate", "w+b", 1),
+  otrl_privkey_generate_FILEp: libotr.declare(
+    "otrl_privkey_generate_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr, ctypes.char.ptr, ctypes.char.ptr
+  ),
+
+  // Begin a private key generation that will potentially take place in
+  // a background thread. This routine must be called from the main
+  // thread. It will set *newkeyp, which you can pass to
+  // otrl_privkey_generate_calculate in a background thread.  If it
+  // returns gcry_error(GPG_ERR_EEXIST), then a privkey creation for
+  // this accountname/protocol is already in progress, and *newkeyp will
+  // be set to NULL.
+  otrl_privkey_generate_start: libotr.declare(
+    "otrl_privkey_generate_start", abi, gcry_error_t,
+    OtrlUserState, ctypes.char.ptr, ctypes.char.ptr, ctypes.void_t.ptr.ptr
+  ),
+
+  // Do the private key generation calculation. You may call this from a
+  // background thread.  When it completes, call
+  // otrl_privkey_generate_finish from the _main_ thread.
+  otrl_privkey_generate_calculate: libotr.declare(
+    "otrl_privkey_generate_calculate", abi, gcry_error_t,
+    ctypes.void_t.ptr
+  ),
+
+  // Call this from the main thread only. It will write the newly created
+  // private key into the given file and store it in the OtrlUserState.
+  otrl_privkey_generate_finish: callWithFILEp.bind(null, "otrl_privkey_generate_finish", "w+b", 2),
+  otrl_privkey_generate_finish_FILEp: libotr.declare(
+    "otrl_privkey_generate_finish_FILEp", abi, gcry_error_t,
+    OtrlUserState, ctypes.void_t.ptr, FILE.ptr
+  ),
+
+  // Call this from the main thread only, in the event that the background
+  // thread generating the key is cancelled. The newkey is deallocated,
+  // and must not be used further.
+  otrl_privkey_generate_cancelled: libotr.declare(
+    "otrl_privkey_generate_cancelled", abi, gcry_error_t,
+    OtrlUserState, ctypes.void_t.ptr
+  ),
+
+  // Read a sets of private DSA keys from a file on disk into the given
+  // OtrlUserState.
+  otrl_privkey_read: callWithFILEp.bind(null, "otrl_privkey_read", "rb", 1),
+  otrl_privkey_read_FILEp: libotr.declare(
+    "otrl_privkey_read_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr
+  ),
+
+  // Read the fingerprint store from a file on disk into the given
+  // OtrlUserState.
+  otrl_privkey_read_fingerprints: callWithFILEp.bind(null, "otrl_privkey_read_fingerprints", "rb", 1),
+  otrl_privkey_read_fingerprints_FILEp: libotr.declare(
+    "otrl_privkey_read_fingerprints_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr, ctypes.void_t.ptr, ctypes.void_t.ptr
+  ),
+
+  // Write the fingerprint store from a given OtrlUserState to a file on disk.
+  otrl_privkey_write_fingerprints: callWithFILEp.bind(null, "otrl_privkey_write_fingerprints", "wb", 1),
+  otrl_privkey_write_fingerprints_FILEp: libotr.declare(
+    "otrl_privkey_write_fingerprints_FILEp", abi, gcry_error_t,
+    OtrlUserState, FILE.ptr
+  ),
+
+  // The length of a string representing a human-readable version of a
+  // fingerprint (including the trailing NUL).
+  OTRL_PRIVKEY_FPRINT_HUMAN_LEN,
+
+  // Human readable fingerprint type
+  fingerprint_t,
+
+  // fingerprint value
+  hash_t,
+
+  // Calculate a human-readable hash of our DSA public key. Return it in the
+  // passed fingerprint buffer. Return NULL on error, or a pointer to the given
+  // buffer on success.
+  otrl_privkey_fingerprint: libotr.declare(
+    "otrl_privkey_fingerprint", abi, ctypes.char.ptr,
+    OtrlUserState, fingerprint_t, ctypes.char.ptr, ctypes.char.ptr
+  ),
+
+  // Convert a 20-byte hash value to a 45-byte human-readable value.
+  otrl_privkey_hash_to_human: libotr.declare(
+    "otrl_privkey_hash_to_human", abi, ctypes.void_t,
+    fingerprint_t, hash_t
+  ),
+
+  // Calculate a raw hash of our DSA public key.  Return it in the passed
+  // fingerprint buffer.  Return NULL on error, or a pointer to the given
+  // buffer on success.
+  otrl_privkey_fingerprint_raw: libotr.declare(
+    "otrl_privkey_fingerprint_raw", abi, ctypes.unsigned_char.ptr,
+    OtrlUserState, hash_t, ctypes.char.ptr, ctypes.char.ptr
+  ),
+
+  // uiOps callbacks
+  policy_cb_t,
+  create_privkey_cb_t,
+  is_logged_in_cb_t,
+  inject_message_cb_t,
+  update_context_list_cb_t,
+  new_fingerprint_cb_t,
+  write_fingerprint_cb_t,
+  gone_secure_cb_t,
+  gone_insecure_cb_t,
+  still_secure_cb_t,
+  max_message_size_cb_t,
+  account_name_cb_t,
+  account_name_free_cb_t,
+  received_symkey_cb_t,
+  otr_error_message_cb_t,
+  otr_error_message_free_cb_t,
+  resent_msg_prefix_cb_t,
+  resent_msg_prefix_free_cb_t,
+  handle_smp_event_cb_t,
+  handle_msg_event_cb_t,
+  create_instag_cb_t,
+  convert_msg_cb_t,
+  convert_free_cb_t,
+  timer_control_cb_t,
+
+  // message.h
+
+  OtrlMessageAppOps,
+
+  errorCode: {
+    OTRL_ERRCODE_NONE: 0,
+    OTRL_ERRCODE_ENCRYPTION_ERROR: 1,
+    OTRL_ERRCODE_MSG_NOT_IN_PRIVATE: 2,
+    OTRL_ERRCODE_MSG_UNREADABLE: 3,
+    OTRL_ERRCODE_MSG_MALFORMED: 4,
+  },
+
+  smpEvent: {
+    OTRL_SMPEVENT_NONE: 0,
+    OTRL_SMPEVENT_ERROR: 1,
+    OTRL_SMPEVENT_ABORT: 2,
+    OTRL_SMPEVENT_CHEATED: 3,
+    OTRL_SMPEVENT_ASK_FOR_ANSWER: 4,
+    OTRL_SMPEVENT_ASK_FOR_SECRET: 5,
+    OTRL_SMPEVENT_IN_PROGRESS: 6,
+    OTRL_SMPEVENT_SUCCESS: 7,
+    OTRL_SMPEVENT_FAILURE: 8,
+  },
+
+  messageEvent: {
+    OTRL_MSGEVENT_NONE: 0,
+    OTRL_MSGEVENT_ENCRYPTION_REQUIRED: 1,
+    OTRL_MSGEVENT_ENCRYPTION_ERROR: 2,
+    OTRL_MSGEVENT_CONNECTION_ENDED: 3,
+    OTRL_MSGEVENT_SETUP_ERROR: 4,
+    OTRL_MSGEVENT_MSG_REFLECTED: 5,
+    OTRL_MSGEVENT_MSG_RESENT: 6,
+    OTRL_MSGEVENT_RCVDMSG_NOT_IN_PRIVATE: 7,
+    OTRL_MSGEVENT_RCVDMSG_UNREADABLE: 8,
+    OTRL_MSGEVENT_RCVDMSG_MALFORMED: 9,
+    OTRL_MSGEVENT_LOG_HEARTBEAT_RCVD: 10,
+    OTRL_MSGEVENT_LOG_HEARTBEAT_SENT: 11,
+    OTRL_MSGEVENT_RCVDMSG_GENERAL_ERR: 12,
+    OTRL_MSGEVENT_RCVDMSG_UNENCRYPTED: 13,
+    OTRL_MSGEVENT_RCVDMSG_UNRECOGNIZED: 14,
+    OTRL_MSGEVENT_RCVDMSG_FOR_OTHER_INSTANCE: 15,
+  },
+
+  convertType: {
+    OTRL_CONVERT_SENDING: 0,
+    OTRL_CONVERT_RECEIVING: 1,
+  },
+
+  // Deallocate a message allocated by other otrl_message_* routines.
+  otrl_message_free: libotr.declare(
+    "otrl_message_free", abi, ctypes.void_t,
+    ctypes.char.ptr
+  ),
+
+  // Handle a message about to be sent to the network.
+  otrl_message_sending: libotr.declare(
+    "otrl_message_sending", abi, gcry_error_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    otrl_instag_t,
+    ctypes.char.ptr,
+    OtrlTLV.ptr,
+    ctypes.char.ptr.ptr,
+    OtrlFragmentPolicy,
+    ConnContext.ptr.ptr,
+    ctypes.void_t.ptr,
+    ctypes.void_t.ptr
+  ),
+
+  // Handle a message just received from the network.
+  otrl_message_receiving: libotr.declare(
+    "otrl_message_receiving", abi, ctypes.int,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr.ptr,
+    OtrlTLV.ptr.ptr,
+    ConnContext.ptr.ptr,
+    ctypes.void_t.ptr,
+    ctypes.void_t.ptr
+  ),
+
+  // Put a connection into the PLAINTEXT state, first sending the
+  // other side a notice that we're doing so if we're currently ENCRYPTED,
+  // and we think he's logged in. Affects only the specified instance.
+  otrl_message_disconnect: libotr.declare(
+    "otrl_message_disconnect", abi, ctypes.void_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    otrl_instag_t
+  ),
+
+  // Call this function every so often, to clean up stale private state that
+  // may otherwise stick around in memory.
+  otrl_message_poll: libotr.declare(
+    "otrl_message_poll", abi, ctypes.void_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr
+  ),
+
+  // Initiate the Socialist Millionaires' Protocol.
+  otrl_message_initiate_smp: libotr.declare(
+    "otrl_message_initiate_smp", abi, ctypes.void_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ConnContext.ptr,
+    ctypes.char.ptr,
+    ctypes.size_t
+  ),
+
+  // Initiate the Socialist Millionaires' Protocol and send a prompt
+  // question to the buddy.
+  otrl_message_initiate_smp_q: libotr.declare(
+    "otrl_message_initiate_smp_q", abi, ctypes.void_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ConnContext.ptr,
+    ctypes.char.ptr,
+    ctypes.char.ptr,
+    ctypes.size_t
+  ),
+
+  // Respond to a buddy initiating the Socialist Millionaires' Protocol.
+  otrl_message_respond_smp: libotr.declare(
+    "otrl_message_respond_smp", abi, ctypes.void_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ConnContext.ptr,
+    ctypes.char.ptr,
+    ctypes.size_t
+  ),
+
+  // Abort the SMP. Called when an unexpected SMP message breaks the
+  // normal flow.
+  otrl_message_abort_smp: libotr.declare(
+    "otrl_message_abort_smp", abi, ctypes.void_t,
+    OtrlUserState,
+    OtrlMessageAppOps.ptr,
+    ctypes.void_t.ptr,
+    ConnContext.ptr
+  ),
+
+  // tlv.h
+
+  tlvs: {
+    OTRL_TLV_PADDING: new ctypes.unsigned_short(0x0000),
+    OTRL_TLV_DISCONNECTED: new ctypes.unsigned_short(0x0001),
+    OTRL_TLV_SMP1: new ctypes.unsigned_short(0x0002),
+    OTRL_TLV_SMP2: new ctypes.unsigned_short(0x0003),
+    OTRL_TLV_SMP3: new ctypes.unsigned_short(0x0004),
+    OTRL_TLV_SMP4: new ctypes.unsigned_short(0x0005),
+    OTRL_TLV_SMP_ABORT: new ctypes.unsigned_short(0x0006),
+    OTRL_TLV_SMP1Q: new ctypes.unsigned_short(0x0007),
+    OTRL_TLV_SYMKEY: new ctypes.unsigned_short(0x0008),
+  },
+
+  OtrlTLV,
+
+  // Return the first TLV with the given type in the chain, or NULL if one
+  // isn't found.
+  otrl_tlv_find: libotr.declare(
+    "otrl_tlv_find", abi, OtrlTLV.ptr,
+    OtrlTLV.ptr, ctypes.unsigned_short
+  ),
+
+  // Deallocate a chain of TLVs.
+  otrl_tlv_free: libotr.declare(
+    "otrl_tlv_free", abi, ctypes.void_t,
+    OtrlTLV.ptr
+  ),
+
+};
+
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["OTRLib"];
new file mode 100644
--- /dev/null
+++ b/chat/modules/OTRUI.jsm
@@ -0,0 +1,711 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["OTRUI"];
+
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+const {
+  XPCOMUtils,
+  l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+
+const {OTR}  = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+const privDialog = "chrome://chat/content/otr-generate-key.xul";
+const authDialog = "chrome://chat/content/otr-auth.xul";
+const addFingerDialog = "chrome://chat/content/otr-add-fingerprint.xul";
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/content/otrUI.properties")
+);
+
+const authVerify = "otr-auth-unverified";
+
+var authLabelMap = new Map([
+  ["otr:auth-error", _("auth.error")],
+  ["otr:auth-success", _("auth.success")],
+  ["otr:auth-successThem", _("auth.successThem")],
+  ["otr:auth-fail", _("auth.fail")],
+  ["otr:auth-waiting", _("auth.waiting")],
+]);
+
+var authTitleMap = new Map([
+  ["otr:auth-error", "error"],
+  ["otr:auth-success", "success"],
+  ["otr:auth-successThem", "successThem"],
+  ["otr:auth-fail", "fail"],
+  ["otr:auth-waiting", "waiting"],
+]);
+
+var trustMap = new Map([
+  [OTR.trustState.TRUST_NOT_PRIVATE, {
+    startLabel: _("start.label"),
+    authLabel: _("auth.label"),
+    disableStart: false,
+    disableEnd: true,
+    disableAuth: true,
+    class: "not_private",
+  }],
+  [OTR.trustState.TRUST_UNVERIFIED, {
+    startLabel: _("refresh.label"),
+    authLabel: _("auth.label"),
+    disableStart: false,
+    disableEnd: false,
+    disableAuth: false,
+    class: "unverified",
+  }],
+  [OTR.trustState.TRUST_PRIVATE, {
+    startLabel: _("refresh.label"),
+    authLabel: _("reauth.label"),
+    disableStart: false,
+    disableEnd: false,
+    disableAuth: false,
+    class: "private",
+  }],
+  [OTR.trustState.TRUST_FINISHED, {
+    startLabel: _("start.label"),
+    authLabel: _("auth.label"),
+    disableStart: false,
+    disableEnd: false,
+    disableAuth: true,
+    class: "finished",
+  }],
+]);
+
+var windowRefs = new Map();
+
+var OTRUI = {
+  globalDoc: null,
+  visibleConv: null,
+
+  debug: true,
+  logMsg(msg) {
+    if (!OTRUI.debug)
+      return;
+    Services.console.logStringMessage(msg);
+  },
+
+  prefs: null,
+  setPrefs() {
+    let branch = "chat.otr.";
+    let prefs = {
+      requireEncryption: false,
+      verifyNudge: true,
+    };
+    let defaults = Services.prefs.getDefaultBranch(branch);
+    Object.keys(prefs).forEach(function(key) {
+      defaults.setBoolPref(key, prefs[key]);
+    });
+    OTRUI.prefs = Services.prefs.getBranch(branch);
+  },
+
+  addMenuObserver() {
+    let iter = Services.ww.getWindowEnumerator();
+    while (iter.hasMoreElements())
+      OTRUI.addMenus(iter.getNext());
+    Services.obs.addObserver(OTRUI, "domwindowopened");
+  },
+
+  removeMenuObserver() {
+    let iter = Services.ww.getWindowEnumerator();
+    while (iter.hasMoreElements())
+      OTRUI.removeMenus(iter.getNext());
+    Services.obs.removeObserver(OTRUI, "domwindowopened");
+  },
+
+  addMenus(win) {
+    let doc = win.document;
+    // Account for unready windows
+    if (doc.readyState !== "complete") {
+      let listen = function() {
+        win.removeEventListener("load", listen);
+        OTRUI.addMenus(win);
+      };
+      win.addEventListener("load", listen);
+    }
+  },
+
+  removeMenus(win) {
+    let doc = win.document;
+    OTRUI.removeBuddyContextMenu(doc);
+  },
+
+  addBuddyContextMenu(buddyContextMenu, doc) {
+    if (!buddyContextMenu || !OTR.libLoaded) {
+      return;  // Not the buddy list context menu
+    }
+    OTRUI.removeBuddyContextMenu(doc);
+
+    let sep = doc.createElement("menuseparator");
+    sep.setAttribute("id", "otrsep");
+    let menuitem = doc.createElement("menuitem");
+    menuitem.setAttribute("label", _("buddycontextmenu.label"));
+    menuitem.setAttribute("id", "otrcont");
+    menuitem.addEventListener("command", () => {
+      let target = buddyContextMenu.triggerNode;
+      if (target.localName == "richlistitem") {
+        let contact = target.contact;
+        let args = OTRUI.contactWrapper(contact);
+        args.wrappedJSObject = args;
+        let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
+        Services.ww.openWindow(null, addFingerDialog, "", features, args);
+      }
+    });
+
+    buddyContextMenu.addEventListener("popupshowing", (e) => {
+      let target = e.target.triggerNode;
+      if (target.localName == "richlistitem") {
+        menuitem.hidden = false;
+        sep.hidden = false;
+      } else { /* probably imconv */
+        menuitem.hidden = true;
+        sep.hidden = true;
+      }
+    });
+
+    buddyContextMenu.appendChild(sep);
+    buddyContextMenu.appendChild(menuitem);
+  },
+
+  removeBuddyContextMenu(doc) {
+    let s = doc.getElementById("otrsep");
+    if (s) {
+      s.remove();
+    }
+    let p = doc.getElementById("otrcont");
+    if (p) {
+      p.remove();
+    }
+  },
+
+  init() {
+    // console.log("====> OTRUI init\n");
+    OTRUI.setPrefs();
+    OTR.init({
+      requireEncryption: OTRUI.prefs.getBoolPref("requireEncryption"),
+      verifyNudge: OTRUI.prefs.getBoolPref("verifyNudge"),
+    });
+    if (!OTR.libLoaded) {
+      return;
+    }
+    OTR.addObserver(OTRUI);
+    OTR.loadFiles().then(function() {
+      Services.obs.addObserver(OTR, "new-ui-conversation");
+      // Disabled until #76 is resolved.
+      // Services.obs.addObserver(OTRUI, "contact-added", false);
+      Services.obs.addObserver(OTRUI, "account-added");
+      // Services.obs.addObserver(OTRUI, "contact-signed-off", false);
+      Services.obs.addObserver(OTRUI, "conversation-loaded");
+      Services.obs.addObserver(OTRUI, "conversation-closed");
+      Services.obs.addObserver(OTRUI, "prpl-quit");
+
+      OTRUI.prefs.addObserver("", OTRUI);
+      let conversations = Services.conversations.getConversations();
+      while (conversations.hasMoreElements()) {
+      let aConv = conversations.getNext();
+      OTRUI.initConv(aConv);
+      }
+      OTRUI.addMenuObserver();
+    }).catch(function(err) {
+      // console.log("===> " + err + "\n");
+      throw err;
+    });
+  },
+
+  disconnect(aConv) {
+    if (aConv)
+      return OTR.disconnect(aConv, true);
+    let allGood = true;
+    let conversations = Services.conversations.getConversations();
+    while (conversations.hasMoreElements()) {
+      let conv = conversations.getNext();
+      if (conv.isChat)
+        continue;
+      if (!OTR.disconnect(conv, true)) {
+        allGood = false;
+      }
+    }
+    return allGood;
+  },
+
+  changePref(aMsg) {
+    switch (aMsg) {
+    case "requireEncryption":
+      OTR.setPolicy(OTRUI.prefs.getBoolPref("requireEncryption"));
+      break;
+    case "verifyNudge":
+      OTR.verifyNudge = OTRUI.prefs.getBoolPref("verifyNudge");
+      break;
+    default:
+      OTRUI.logMsg(aMsg);
+    }
+  },
+
+  openAuth(window, name, mode, uiConv, contactInfo) {
+    let otrAuth = this.globalDoc.querySelector(".otr-auth");
+    otrAuth.disabled = true;
+    let win = window.openDialog(
+      authDialog,
+      "auth=" + name,
+      "centerscreen,resizable=no,minimizable=no",
+      mode,
+      uiConv,
+      contactInfo
+    );
+    windowRefs.set(name, win);
+    window.addEventListener("beforeunload", function() {
+      otrAuth.disabled = false;
+      windowRefs.delete(name);
+    });
+  },
+
+  closeAuth(context) {
+    let win = windowRefs.get(context.username);
+    if (win)
+      win.close();
+  },
+
+  noOtrPossible(otrContainer, context) {
+    otrContainer.hidden = true;
+
+    if (context) {
+      OTRUI.hideUserNotifications(context);
+    } else {
+      OTRUI.hideAllNotifications();
+    }
+  },
+
+  sendSystemAlert(uiConv, conv, bundleId) {
+    uiConv.systemMessage(_(bundleId, conv.normalizedName));
+  },
+
+  setNotificationBox(notificationbox) {
+    this.globalBox = notificationbox;
+  },
+
+/*
+ *  possible states:
+ *    tab isn't a 1:1, isChat == true
+ *      then OTR isn't possible, hide the button
+ *    tab is a 1:1, isChat == false
+ *      no conversation active, uiConv cannot be found
+ *        then OTR isn't possible YET, hide the button
+ *      conversation active, uiConv found
+ *        disconnected?
+ *          could the other side come back? should we keep the button?
+ *        set the state based on the OTR library state
+ */
+
+  addButton(aObject) {
+    this.globalDoc = aObject.ownerDocument;
+    let _conv = aObject._conv;
+    OTRUI.setMsgState(_conv, null, this.globalDoc, true);
+  },
+
+  hideOTRButton() {
+    if (!OTR.libLoaded)
+      return;
+    if (!this.globalDoc)
+      return;
+    OTRUI.visibleConv = null;
+    let otrContainer = this.globalDoc.querySelector(".otr-container");
+    OTRUI.noOtrPossible(otrContainer);
+  },
+
+  updateOTRButton(_conv) {
+    if (!OTR.libLoaded)
+      return;
+    if (!this.globalDoc)
+      return;
+    OTRUI.visibleConv = _conv;
+    let convBinding =
+      this.globalDoc.getElementById("conversationsDeck").selectedPanel;
+    if (convBinding && convBinding._conv && convBinding._conv.target) {
+      OTRUI.setMsgState(_conv, null, this.globalDoc, false);
+    } else {
+      this.hideOTRButton();
+    }
+  },
+
+  // set msg state on toolbar button
+  setMsgState(_conv, context, doc, addSystemMessage) {
+    if (!this.visibleConv) {
+      return;
+    }
+    if (_conv != null && !(_conv === this.visibleConv)) {
+      return;
+    }
+
+    let otrContainer = doc.querySelector(".otr-container");
+    let otrButton = doc.querySelector(".otr-button");
+    if (_conv != null && _conv.isChat) {
+      OTRUI.noOtrPossible(otrContainer, context);
+      return;
+    }
+
+    if (!context && _conv != null) {
+      context = OTR.getContext(_conv);
+      if (!context) {
+        OTRUI.noOtrPossible(otrContainer, null);
+      }
+    }
+
+    try {
+      let uiConv = OTR.getUIConvFromContext(context);
+      if (uiConv != null && !(uiConv === this.visibleConv)) {
+        return;
+      }
+
+      if (uiConv.isChat) {
+        OTRUI.noOtrPossible(otrContainer, context);
+        return;
+      }
+      if (addSystemMessage) {
+        let trust = OTRUI.getTrustSettings(context);
+        uiConv.systemMessage(_("state." + trust.class, context.username));
+      }
+    } catch (e) {
+      OTRUI.noOtrPossible(otrContainer, context);
+      return;
+    }
+
+    otrContainer.hidden = false;
+    let otrStart = doc.querySelector(".otr-start");
+    let otrEnd = doc.querySelector(".otr-end");
+    let otrAuth = doc.querySelector(".otr-auth");
+    let trust = OTRUI.getTrustSettings(context);
+    otrButton.setAttribute("tooltiptext", _("state." + trust.class, context.username));
+    otrButton.setAttribute("label", _("state." + trust.class + ".label"));
+    otrButton.className = "otr-button otr-" + trust.class;
+    otrStart.setAttribute("label", trust.startLabel);
+    otrStart.setAttribute("disabled", trust.disableStart);
+    otrEnd.setAttribute("disabled", trust.disableEnd);
+    otrAuth.setAttribute("label", trust.authLabel);
+    otrAuth.setAttribute("disabled", trust.disableAuth);
+    OTRUI.hideAllNotifications();
+    OTRUI.showUserNotifications(context);
+  },
+
+  alertTrust(context) {
+    let uiConv = OTR.getUIConvFromContext(context);
+    let trust = OTRUI.getTrustSettings(context);
+    uiConv.systemMessage(_("afterauth." + trust.class, context.username));
+  },
+
+  getTrustSettings(context) {
+    let result = trustMap.get(OTR.trust(context));
+    return result;
+  },
+
+  askAuth(aObject) {
+    let uiConv = OTR.getUIConvFromContext(aObject.context);
+    if (!uiConv) return;
+
+    let window = this.globalDoc.defaultView;
+    let name = uiConv.target.normalizedName;
+    OTRUI.openAuth(window, name, "ask", uiConv, aObject);
+  },
+
+  closeUnverified(context) {
+    let uiConv = OTR.getUIConvFromContext(context);
+    if (!uiConv) return;
+
+    let notification = this.globalBox.getNotificationWithValue(authVerify);
+    if (notification)
+      notification.close();
+  },
+
+  hideUserNotifications(context) {
+    let notifications = this.globalBox.allNotifications;
+    for (let i = notifications.length - 1; i >= 0; i--) {
+      if (context.username == notifications[i].getAttribute("user")) {
+        notifications[i].setAttribute("hidden", "true");
+      }
+    }
+  },
+
+  hideAllNotifications() {
+    let notifications = this.globalBox.allNotifications;
+    for (let i = notifications.length - 1; i >= 0; i--) {
+      notifications[i].setAttribute("hidden", "true");
+    }
+  },
+
+  showUserNotifications(context) {
+    let notifications = this.globalBox.allNotifications;
+    for (let i = notifications.length - 1; i >= 0; i--) {
+      if (context.username == notifications[i].getAttribute("user"))
+        notifications[i].removeAttribute("hidden");
+    }
+  },
+
+  notifyUnverified(context, seen) {
+    let uiConv = OTR.getUIConvFromContext(context);
+    if (!uiConv) return;
+
+    if (this.globalBox.getNotificationWithValue(authVerify))
+      return;
+
+    let window = this.globalDoc.defaultView;
+
+    let msg = _("finger." + seen, context.username);
+    let buttons = [{
+      label: _("finger.verify"),
+      accessKey: _("verify.accessKey"),
+      callback() {
+        let name = uiConv.target.normalizedName;
+        OTRUI.openAuth(window, name, "start", uiConv);
+        // prevent closing of notification bar when the button is hit
+        return true;
+      },
+    }];
+
+    let priority = this.globalBox.PRIORITY_WARNING_MEDIUM;
+    this.globalBox.appendNotification(msg, authVerify, null, priority, buttons, null);
+
+    this.updateNotificationUI(context, "verify", authVerify);
+  },
+
+  updateNotificationUI(context, type, value) {
+    let notification = this.globalBox.getNotificationWithValue(value);
+    notification.setAttribute("user", context.username);
+    notification.setAttribute("orient", "vertical");
+    notification.messageDetails.setAttribute("orient", "vertical");
+    notification.messageDetails.removeAttribute("oncommand");
+    notification.messageDetails.removeAttribute("align");
+
+    let title = this.globalDoc.createElement("title");
+    title.setAttribute("flex", "1");
+    title.setAttribute("crop", "end");
+    title.textContent = _(type + ".title");
+
+    let close = notification.querySelector("toolbarbutton");
+    close.setAttribute("oncommand", "this.parentNode.parentNode.dismiss();");
+
+    let top = this.globalDoc.createElement("hbox");
+    top.setAttribute("flex", "1");
+    top.setAttribute("align", "center");
+    top.classList.add("otr-notification-header");
+    top.appendChild(notification.messageImage);
+    top.appendChild(title);
+    top.appendChild(close);
+    notification.insertBefore(top, notification.messageDetails);
+
+    let bottom = this.globalDoc.createElement("hbox");
+    bottom.setAttribute("flex", "1");
+    bottom.setAttribute("oncommand", "this.parentNode._doButtonCommand(event);");
+    bottom.classList.add("otr-notification-footer");
+
+    notification.querySelectorAll("button").forEach((e) => {
+      bottom.appendChild(e);
+    });
+
+    notification.appendChild(bottom);
+  },
+
+  closeVerification(context) {
+    let uiConv = OTR.getUIConvFromContext(context);
+    if (!uiConv) return;
+
+    authLabelMap.forEach(function(_, key) {
+      let prevNotification = OTRUI.globalBox.getNotificationWithValue(key);
+      if (prevNotification)
+        prevNotification.close();
+    });
+  },
+
+  notifyVerification(context, key, cancelable) {
+    let uiConv = OTR.getUIConvFromContext(context);
+    if (!uiConv) return;
+
+    // TODO: maybe update the .label property on the notification instead
+    // of closing it ... although, buttons need to be updated too.
+    OTRUI.closeVerification(context);
+
+    let msg = authLabelMap.get(key);
+    let type = authTitleMap.get(key);
+    let buttons = [];
+    if (cancelable) {
+      buttons = [{
+        label: _("auth.cancel"),
+        accessKey: _("auth.cancelAccessKey"),
+        callback() {
+          let context = OTR.getContext(uiConv.target);
+          OTR.abortSMP(context);
+        },
+      }];
+    }
+
+    // higher priority to overlay the current notifyUnverified
+    let priority = this.globalBox.PRIORITY_WARNING_HIGH;
+    OTRUI.closeUnverified(context);
+    this.globalBox.appendNotification(msg, key, null, priority, buttons, null);
+
+    this.updateNotificationUI(context, type, key);
+  },
+
+  updateAuth(aObj) {
+    // let uiConv = OTR.getUIConvFromContext(aObj.context);
+    if (!aObj.progress) {
+      OTRUI.closeAuth(aObj.context);
+      OTRUI.notifyVerification(aObj.context, "otr:auth-error", false);
+    } else if (aObj.progress === 100) {
+      let key;
+      if (aObj.success) {
+        if (aObj.context.trust) {
+          key = "otr:auth-success";
+          OTR.notifyTrust(aObj.context);
+        } else {
+          key = "otr:auth-successThem";
+        }
+      } else {
+        key = "otr:auth-fail";
+        if (!aObj.context.trust)
+          OTR.notifyTrust(aObj.context);
+      }
+      OTRUI.notifyVerification(aObj.context, key, false);
+    } else {
+      // TODO: show the aObj.progress to the user with a
+      //   <progressmeter mode="determined" value="10" />
+      OTRUI.notifyVerification(aObj.context, "otr:auth-waiting", true);
+    }
+  },
+
+  generate(args) {
+    let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
+    args.wrappedJSObject = args;
+    Services.ww.openWindow(null, privDialog, "", features, args);
+  },
+
+  onAccountCreated(acc) {
+    let account = acc.normalizedName;
+    let protocol = acc.protocol.normalizedName;
+    Promise.resolve();
+    if (OTR.privateKeyFingerprint(account, protocol) === null)
+      OTR.generatePrivateKey(account, protocol);
+  },
+
+  contactWrapper(contact) {
+    let wrapper = {
+      account: contact.preferredBuddy.preferredAccountBuddy.account.normalizedName,
+      protocol: contact.preferredBuddy.protocol.normalizedName,
+      screenname: contact.preferredBuddy.preferredAccountBuddy.userName,
+    };
+    return wrapper;
+  },
+
+  onContactAdded(contact) {
+    let args = OTRUI.contactWrapper(contact);
+    if (OTR.getFingerprintsForRecipient(args.account, args.protocol, args.screenname).length > 0)
+      return;
+    args.wrappedJSObject = args;
+    let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
+    Services.ww.openWindow(null, addFingerDialog, "", features, args);
+  },
+
+  observe(aObject, aTopic, aMsg) {
+    let doc;
+    // console.log("====> observing topic: " + aTopic + " with msg: " + aMsg);
+    // console.log(aObject);
+
+    switch (aTopic) {
+    case "nsPref:changed":
+      OTRUI.changePref(aMsg);
+      break;
+    case "conversation-loaded":
+      doc = aObject.ownerDocument;
+      let windowtype = doc.documentElement.getAttribute("windowtype");
+      if (windowtype !== "mail:3pane") {
+        return;
+      }
+      OTRUI.addButton(aObject);
+      break;
+    case "conversation-closed":
+      if (aObject.isChat)
+        return;
+      this.globalBox.removeAllNotifications();
+      OTRUI.closeAuth(OTR.getContext(aObject));
+      OTRUI.disconnect(aObject);
+      break;
+    // case "contact-signed-off":
+    //  break;
+    case "prpl-quit":
+      OTRUI.disconnect(null);
+      break;
+    case "domwindowopened":
+      OTRUI.addMenus(aObject);
+      break;
+    case "otr:generate":
+      OTRUI.generate(aObject);
+      break;
+    case "otr:disconnected":
+    case "otr:msg-state":
+      if (aTopic === "otr:disconnected" ||
+          OTR.trust(aObject) !== OTR.trustState.TRUST_UNVERIFIED) {
+        OTRUI.closeAuth(aObject);
+        OTRUI.closeUnverified(aObject);
+        OTRUI.closeVerification(aObject);
+      }
+      OTRUI.setMsgState(null, aObject, this.globalDoc, false);
+      break;
+    case "otr:unverified":
+      OTRUI.notifyUnverified(aObject, aMsg);
+      break;
+    case "otr:trust-state":
+      OTRUI.alertTrust(aObject);
+      break;
+    case "otr:log":
+      OTRUI.logMsg("otr: " + aObject);
+      break;
+    case "account-added":
+      OTRUI.onAccountCreated(aObject);
+      break;
+    case "contact-added":
+      OTRUI.onContactAdded(aObject);
+      break;
+    case "otr:auth-ask":
+      OTRUI.askAuth(aObject);
+      break;
+    case "otr:auth-update":
+      OTRUI.updateAuth(aObject);
+      break;
+    }
+  },
+
+  initConv(binding) {
+    OTR.addConversation(binding._conv);
+    OTRUI.addButton(binding);
+  },
+
+  resetConv(binding) {
+    OTR.removeConversation(binding._conv);
+    let otrButton = this.globalDoc.querySelector(".otr-button");
+    if (!otrButton)
+      return;
+    otrButton.remove();
+  },
+
+  destroy() {
+    if (!OTR.libLoaded)
+      return;
+    OTRUI.disconnect(null);
+    Services.obs.removeObserver(OTR, "new-ui-conversation");
+    // Services.obs.removeObserver(OTRUI, "contact-added");
+    // Services.obs.removeObserver(OTRUI, "contact-signed-off");
+    Services.obs.removeObserver(OTRUI, "account-added");
+    Services.obs.removeObserver(OTRUI, "conversation-loaded");
+    Services.obs.removeObserver(OTRUI, "conversation-closed");
+    Services.obs.removeObserver(OTRUI, "prpl-quit");
+
+    let conversations = Services.conversations.getConversations();
+    while (conversations.hasMoreElements()) {
+      OTRUI.resetConv(conversations.getNext());
+    }
+    OTRUI.prefs.removeObserver("", OTRUI);
+    OTR.removeObserver(OTRUI);
+    OTR.close();
+    OTRUI.removeMenuObserver();
+  },
+
+};
--- a/chat/modules/moz.build
+++ b/chat/modules/moz.build
@@ -3,22 +3,27 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
 EXTRA_JS_MODULES += [
     'ArrayBufferUtils.jsm',
     'BigInteger.jsm',
+    'CLib.jsm',
     'DNS.jsm',
     'hiddenWindow.jsm',
     'imContentSink.jsm',
     'imServices.jsm',
     'imSmileys.jsm',
     'imStatusUtils.jsm',
     'imTextboxUtils.jsm',
     'imThemes.jsm',
     'imXPCOMUtils.jsm',
     'jsProtoHelper.jsm',
     'NormalizedMap.jsm',
+    'OTR.jsm',
+    'OTRHelpers.jsm',
+    'OTRLib.jsm',
+    'OTRUI.jsm',
     'socket.jsm',
     'ToLocaleFormat.jsm',
 ]
new file mode 100644
--- /dev/null
+++ b/chat/themes/icons/otr-connection-encrypted.svg
@@ -0,0 +1,7 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M12,7 L13,7 C13.5522847,7 14,7.44771525 14,8 L14,14 C14,14.5522847 13.5522847,15 13,15 L3,15 C2.44771525,15 2,14.5522847 2,14 L2,8 C2,7.44771525 2.44771525,7 3,7 L4,7 L4,5.00032973 C4,2.79202307 5.79321704,1 8,1 C10.2075938,1 12,2.79481161 12,5.00032973 L12,7 Z M10,7 L10,5.00032973 C10,3.89878113 9.10242341,3 8,3 C6.89748845,3 6,3.89689088 6,5.00032973 L6,7 L10,7 Z"/>
+  <path style="fill:#00b22c;fill-opacity:1;stroke-width:0.99999988" d="M 9.0226853,15.999039 A 0.8763328,0.8763328 0 0 1 8.403118,15.742273 L 5.7741199,13.113275 A 0.8763328,0.8763328 0 0 1 7.0132545,11.87414 l 1.8902496,1.89025 5.5349179,-7.9071509 a 0.8763328,0.8763328 0 0 1 1.436309,1.0042774 L 9.7404016,15.624844 a 0.8763328,0.8763328 0 0 1 -0.6414753,0.374195 0.75627521,0.75627521 0 0 1 -0.076241,0 z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/chat/themes/icons/otr-connection-finished.svg
@@ -0,0 +1,7 @@
+<!-- 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 http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M12,7 L13,7 C13.5522847,7 14,7.44771525 14,8 L14,14 C14,14.5522847 13.5522847,15 13,15 L3,15 C2.44771525,15 2,14.5522847 2,14 L2,8 C2,7.44771525 2.44771525,7 3,7 L4,7 L4,5.00032973 C4,2.79202307 5.79321704,1 8,1 C10.2075938,1 12,2.79481161 12,5.00032973 L12,7 Z M10,7 L10,5.00032973 C10,3.89878113 9.10242341,3 8,3 C6.89748845,3 6,3.89689088 6,5.00032973 L6,7 L10,7 Z"/>
+  <path d="M 11.936582,10.734873 15.767811,6.9036441 A 0.8280753,0.8280753 0 0 0 14.59636,5.7332977 L 10.765132,9.5634223 6.9339034,5.7332977 A 0.8280753,0.8280753 0 1 0 5.763557,6.9036441 L 9.5936809,10.734873 5.763557,14.566099 a 0.8280753,0.8280753 0 1 0 1.1703464,1.170347 l 3.8312286,-3.830123 3.831228,3.831227 a 0.8280753,0.8280753 0 0 0 1.170347,-1.171451 z" style="fill:#ff9400;fill-opacity:1;stroke-width:1"/>
+</svg>
--- a/chat/themes/jar.mn
+++ b/chat/themes/jar.mn
@@ -15,14 +15,17 @@ chat.jar:
 	skin/classic/chat/unknown.png
 	skin/classic/chat/unknown-16.png
 	skin/classic/chat/chat-16.png
 	skin/classic/chat/chat-left-16.png
 	skin/classic/chat/conv.css
 	skin/classic/chat/browserRequest.css
 	skin/classic/chat/imtooltip.css
 	skin/classic/chat/status.css
+	skin/classic/chat/otr.css
 	skin/classic/chat/prpl-generic/icon32.png	(icons/prpl-generic-32.png)
 	skin/classic/chat/prpl-generic/icon48.png	(icons/prpl-generic-48.png)
 	skin/classic/chat/prpl-generic/icon.png		(icons/prpl-generic.png)
 	skin/classic/chat/prpl-unknown/icon32.png	(icons/prpl-unknown-32.png)
 	skin/classic/chat/prpl-unknown/icon48.png	(icons/prpl-unknown-48.png)
 	skin/classic/chat/prpl-unknown/icon.png		(icons/prpl-unknown.png)
+	skin/classic/chat/otr-connection-encrypted.svg (icons/otr-connection-encrypted.svg)
+	skin/classic/chat/otr-connection-finished.svg  (icons/otr-connection-finished.svg)
new file mode 100644
--- /dev/null
+++ b/chat/themes/otr.css
@@ -0,0 +1,142 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+.otr-container {
+  border-top: 1px solid var(--splitter-color);
+  min-height: 32px;
+  padding: 4px;
+}
+
+.otr-label {
+  font-weight: 600;
+}
+
+.otr-not_private > image {
+  list-style-image: url("chrome://messenger/skin/icons/connection-insecure.svg");
+}
+
+.otr-unverified > image {
+  list-style-image: url("chrome://messenger/skin/icons/connection-mixed.svg");
+}
+
+.otr-finished > image {
+  list-style-image: url("chrome://chat/skin/otr-connection-finished.svg");
+}
+
+.otr-private > image {
+  list-style-image: url("chrome://chat/skin/otr-connection-encrypted.svg");
+}
+
+toolbarbutton.otr-button {
+  -moz-appearance: button !important;
+  padding: 1px !important;
+}
+
+.otr-button > image {
+  margin-inline-end: 3px;
+  width: 14px;
+}
+
+.otr-button .toolbarbutton-menu-dropmarker {
+  -moz-appearance: none !important;
+  list-style-image: none;
+  margin-left: 0;
+  margin-right: 0;
+  margin-inline-start: 3px;
+  width: 9px;
+}
+
+.otr-button .toolbarbutton-menu-dropmarker > .dropmarker-icon {
+  width: 17px;
+  height: 7px;
+  background-image: url("chrome://messenger/skin/icons/toolbarbutton-arrow.svg");
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 9px 7px;
+}
+
+/* otr botificationbox tweaks */
+
+#otr-notification-box notification[type="warning"] {
+  background: #FFF2BE !important;
+}
+
+#otr-notification-box notification {
+  padding-inline-start: 6px !important;
+  padding: 6px !important;
+  border-top: 1px solid var(--splitter-color) !important;
+  border-bottom: none !important;
+}
+
+#otr-notification-box .messageImage {
+  margin-inline-end: 6px !important;
+  color: #9E650C;
+}
+
+#otr-notification-box .messageText {
+  margin-inline-start: 6px !important;
+  margin-bottom: 6px !important;
+}
+
+.otr-notification-header {
+  display: inherit;
+  padding: 3px 6px;
+}
+
+.otr-notification-header title {
+  font-weight: bold;
+  color: #9E650C;
+  flex-grow: 1;
+}
+
+.otr-notification-header .messageCloseButton {
+  min-height: 20px;
+}
+
+.otr-notification-header .messageCloseButton > .toolbarbutton-icon {
+  margin-inline-end: 0px !important;
+}
+
+.otr-notification-footer {
+  display: flex;
+  justify-content: end;
+}
+
+/* waiting */
+#otr-notification-box notification[type="warning"][value="otr:auth-waiting"] >
+  hbox > .messageImage {
+  list-style-image: url("chrome://global/skin/icons/help.svg") !important;
+}
+
+/* fail */
+#otr-notification-box notification[type="warning"][value="otr:auth-fail"] {
+  background: #ffc9d5 !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-fail"] >
+  hbox > .messageImage {
+  list-style-image: url("chrome://global/skin/icons/error.svg") !important;
+  color: #c93434 !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-fail"]
+  .otr-notification-header title {
+  color: #c93434 !important;
+}
+
+/* success */
+#otr-notification-box notification[type="warning"][value="otr:auth-success"] {
+  background: #D3F4AF !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-success"] >
+  hbox > .messageImage {
+  list-style-image: url("chrome://global/skin/icons/check.svg") !important;
+  color: #407501 !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-success"]
+  .otr-notification-header title {
+  color: #407501 !important;
+}
--- a/mail/app/profile/all-thunderbird.js
+++ b/mail/app/profile/all-thunderbird.js
@@ -714,16 +714,18 @@ pref("mail.chat.show_desktop_notificatio
 // 1 == Show sender's info only (not message preview),
 // 2 == No info (fill dummy values).
 pref("mail.chat.notification_info", 0);
 pref("mail.chat.play_sound", true);
 // 0 == default system sound, 1 == user specified wav
 pref("mail.chat.play_sound.type", 0);
 // if sound is user specified, this needs to be a file url
 pref("mail.chat.play_sound.url", "");
+// Enable/Disable support for OTR chat encryption.
+pref("chat.otr.enable", false);
 
 // BigFiles
 pref("mail.cloud_files.enabled", true);
 pref("mail.cloud_files.inserted_urls.footer.link", "https://www.thunderbird.net");
 pref("mail.cloud_files.learn_more_url", "https://support.thunderbird.net/kb/filelink-large-attachments");
 
 // Ignore threads
 pref("mail.ignore_thread.learn_more_url", "https://support.thunderbird.net/kb/ignore-threads");
--- a/mail/components/im/content/chat-conversation-info.js
+++ b/mail/components/im/content/chat-conversation-info.js
@@ -1,15 +1,29 @@
 /* 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 http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-/* global MozXULElement */
+var {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "OTR", "resource:///modules/OTR.jsm");
+ChromeUtils.defineModuleGetter(this, "OTRUI", "resource:///modules/OTRUI.jsm");
+
+/* globals MozElements MozXULElement */
+
+const gNotification = {};
+XPCOMUtils.defineLazyGetter(gNotification, "notificationbox", () => {
+  return new MozElements.NotificationBox(element => {
+    element.setAttribute("flex", "1");
+    document.getElementById("otr-notification-box").append(element);
+  });
+});
 
 /**
  * The MozChatConversationInfo widget displays information about a chat:
  * e.g. the channel name and topic of an IRC channel, or nick, user image and
  * status of a conversation partner.
  * It is typically shown at the top right of the chat UI.
  * @extends {MozXULElement}
  */
@@ -24,34 +38,64 @@ class MozChatConversationInfo extends Mo
       ".statusMessage": "value=statusMessage,tooltiptext=statusTooltiptext,editable=topicEditable,editing,noTopic",
     };
   }
 
   connectedCallback() {
     if (this.hasChildNodes() || this.delayConnectedCallback()) {
       return;
     }
+    this.setAttribute("orient", "vertical");
+
     this.appendChild(MozXULElement.parseXULToFragment(`
-      <stack class="statusImageStack">
-        <box class="userIconHolder">
-          <image class="userIcon" mousethrough="always"></image>
-        </box>
-        <image class="statusTypeIcon"></image>
-      </stack>
-      <stack class="displayNameAndstatusMessageStack" mousethrough="always" flex="1">
-        <hbox align="center" flex="1">
-          <description class="displayName" flex="1" crop="end">
+      <hbox class="displayUserAccount" flex="1">
+        <stack class="statusImageStack">
+          <box class="userIconHolder">
+            <image class="userIcon" mousethrough="always"></image>
+          </box>
+          <image class="statusTypeIcon"></image>
+        </stack>
+        <stack class="displayNameAndstatusMessageStack" mousethrough="always" flex="1">
+          <hbox align="center" flex="1">
+            <description class="displayName" flex="1" crop="end">
+            </description>
+            <image class="prplIcon"></image>
+          </hbox>
+          <description class="statusMessage" mousethrough="never" crop="end" flex="100000">
           </description>
-          <image class="prplIcon"></image>
-        </hbox>
-        <description class="statusMessage" mousethrough="never" crop="end" flex="100000">
-        </description>
-      </stack>
-    `));
+        </stack>
+      </hbox>
+      <hbox class="otr-container" align="left" valign="middle" flex="1" hidden="true">
+        <label class="otr-label" crop="end" value="&state.label;" flex="1"/>
+        <toolbarbutton id="otrButton"
+                       mode="dialog"
+                       class="otr-button toolbarbutton-1"
+                       type="menu"
+                       label="Insecure"
+                       tooltiptext="&start.label;">
+          <menupopup class="otr-menu-popup">
+            <menuitem class="otr-start" label="&start.label;"
+                      oncommand='this.closest("chat-conversation-info").onOtrStartClicked();'/>
+            <menuitem class="otr-end" label="&end.label;"
+                      oncommand='this.closest("chat-conversation-info").onOtrEndClicked();'/>
+            <menuitem class="otr-auth" label="&auth.label;"
+                      oncommand='this.closest("chat-conversation-info").onOtrAuthClicked();'/>
+          </menupopup>
+        </toolbarbutton>
+      </hbox>
+      <hbox id="otr-notification-box"></hbox>
+    `, ["chrome://chat/content/otr-chat.dtd"]));
+
     this.topic.addEventListener("click", this.startEditTopic.bind(this));
+
+    if (Services.prefs.getBoolPref("chat.otr.enable")) {
+      let otrButton = this.querySelector(".otr-button");
+      otrButton.addEventListener("command", this.otrButtonClicked);
+      OTRUI.setNotificationBox(gNotification.notificationbox);
+    }
     this.initializeAttributeInheritance();
   }
 
   get topic() {
     return this.querySelector(".statusMessage");
   }
 
   finishEditTopic(save) {
@@ -111,10 +155,45 @@ class MozChatConversationInfo extends Mo
     this._topicBlur = this.topicBlur.bind(this);
     elt.addEventListener("blur", this._topicBlur);
     elt.getBoundingClientRect();
     if (this.hasAttribute("noTopic")) {
       elt.value = "";
     }
     elt.select();
   }
+
+  otrButtonClicked(aEvent) {
+    aEvent.preventDefault();
+    let otrMenu = this.querySelector(".otr-menu-popup");
+    otrMenu.openPopup(otrMenu.parentNode, "after_start");
+  }
+
+  onOtrStartClicked() {
+    // check if start-menu-command is disabled, if yes exit
+    let convBinding = document.getElementById("conversationsDeck").selectedPanel;
+    let uiConv = convBinding._conv;
+    let conv = uiConv.target;
+    let context = OTR.getContext(conv);
+    let bundleId = "alert." + (
+      context.msgstate === OTR.getMessageState().OTRL_MSGSTATE_ENCRYPTED ?
+        "refresh" : "start");
+    OTRUI.sendSystemAlert(uiConv, conv, bundleId);
+    OTR.sendQueryMsg(conv);
+  }
+
+  onOtrEndClicked() {
+    let convBinding = document.getElementById("conversationsDeck").selectedPanel;
+    let uiConv = convBinding._conv;
+    let conv = uiConv.target;
+    OTR.disconnect(conv, false);
+    let bundleId = "alert.gone_insecure";
+    OTRUI.sendSystemAlert(uiConv, conv, bundleId);
+  }
+
+  onOtrAuthClicked() {
+    let convBinding = document.getElementById("conversationsDeck").selectedPanel;
+    let uiConv = convBinding._conv;
+    let conv = uiConv.target;
+    OTRUI.openAuth(window, conv.normalizedName, "start", uiConv);
+  }
 }
 customElements.define("chat-conversation-info", MozChatConversationInfo);
--- a/mail/components/im/content/chat-messenger.inc.xul
+++ b/mail/components/im/content/chat-messenger.inc.xul
@@ -88,18 +88,17 @@
                              type="menu"
                              class="toolbarbutton-1 button-appmenu"
                              label="&appmenuButton.label;"
                              tooltiptext="&appmenuButton1.tooltip;"/>
             </toolbarpalette>
             <toolbarset id="customChatToolbars" context="chat-toolbar-context-menu"/>
           </toolbox>
 
-          <vbox id="chat-notification-top" flex="1">
-            <!-- notificationbox will be added here lazily. -->
+          <vbox flex="1">
             <hbox id="chatPanel" flex="1">
               <vbox id="listPaneBox" minwidth="125" width="200" persist="width">
                 <richlistbox id="contactlistbox"
                              context="buddyListContextMenu"
                              tooltip="imTooltip" flex="1">
                   <richlistitem is="chat-group" id="conversationsGroup"
                                 name="&conversationsHeader.label;"/>
                   <richlistitem is="chat-imconv" id="searchResultConv"
@@ -107,69 +106,75 @@
                   <richlistitem is="chat-group" id="onlinecontactsGroup"
                                 name="&onlineContactsHeader.label;"/>
                   <richlistitem is="chat-group" id="offlinecontactsGroup"
                                 name="&offlineContactsHeader.label;"
                                 class="closed"/>
                 </richlistbox>
               </vbox>
               <splitter id="listSplitter" collapse="before"/>
-              <deck id="conversationsDeck" flex="1">
-                <vbox flex="1" id="noConvScreen" class="im-placeholder-screen" align="center" pack="center">
-                  <hbox id="noConvBox" class="im-placeholder-box" align="top">
-                    <vbox id="noConvInnerBox" class="im-placeholder-innerbox" flex="1">
-                      <label id="noConvTitle" class="im-placeholder-title">&chat.noConv.title;</label>
-                      <description id="noConvDesc"
-                                   class="im-placeholder-desc">&chat.noConv.description;</description>
-                    </vbox>
-                    <vbox id="noAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
-                      <label id="noAccountTitle" class="im-placeholder-title">&chat.noAccount.title;</label>
-                      <description id="noAccountDesc"
-                                   class="im-placeholder-desc">&chat.noAccount.description;</description>
-                      <hbox class="im-placeholder-button-box" flex="1">
-                        <spacer flex="1"/>
-                        <button id="openIMAccountWizardButton" label="&chat.accountWizard.button;"
-                                oncommand="openIMAccountWizard();"/>
-                      </hbox>
-                    </vbox>
-                    <vbox id="noConnectedAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
-                      <label id="noConnectedAccountTitle"
-                             class="im-placeholder-title">&chat.noConnectedAccount.title;</label>
-                      <description id="noConnectedAccountDesc"
-                                   class="im-placeholder-desc">&chat.noConnectedAccount.description;</description>
-                      <hbox class="im-placeholder-button-box" flex="1">
-                        <spacer flex="1"/>
-                        <button id="openIMAccountManagerButton" label="&chat.showAccountManager.button;"
-                                oncommand="openIMAccountMgr();"/>
-                      </hbox>
-                    </vbox>
-                  </hbox>
-                </vbox>
-                <vbox id="logDisplay" flex="1">
-                  <deck id="logDisplayDeck" flex="1">
-                    <vbox flex="1" id="noPreviousConvScreen" class="im-placeholder-screen" align="center" pack="center">
-                      <hbox id="noPreviousConvBox" class="im-placeholder-box" align="top">
-                        <vbox id="noPreviousConvInnerBox" class="im-placeholder-innerbox" flex="1">
-                          <description id="noPreviousConvDesc"
-                                       class="im-placeholder-desc">&chat.noPreviousConv.description;</description>
-                        </vbox>
-                      </hbox>
-                    </vbox>
-                    <vbox flex="1" id="logDisplayBrowserBox">
-                      <browser id="conv-log-browser" is="conversation-browser" type="content"
-                               contextmenu="chatConversationContextMenu" flex="1"
-                               tooltip="imTooltip"/>
-                      <html:progress id="log-browserProgress" max="100" hidden="true"/>
-                      <findbar id="log-findbar" browserid="conv-log-browser"/>
-                    </vbox>
-                  </deck>
-                  <button id="goToConversation" hidden="true"
-                          oncommand="chatHandler.showCurrentConversation();"/>
-                </vbox>
-              </deck>
+              <vbox id="chat-notification-top" flex="1">
+                <!-- notificationbox will be added here lazily. -->
+                <deck id="conversationsDeck" flex="1">
+
+                  <vbox flex="1" id="noConvScreen" class="im-placeholder-screen" align="center" pack="center">
+                    <hbox id="noConvBox" class="im-placeholder-box" align="top">
+                      <vbox id="noConvInnerBox" class="im-placeholder-innerbox" flex="1">
+                        <label id="noConvTitle" class="im-placeholder-title">&chat.noConv.title;</label>
+                        <description id="noConvDesc"
+                                     class="im-placeholder-desc">&chat.noConv.description;</description>
+                      </vbox>
+                      <vbox id="noAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
+                        <label id="noAccountTitle" class="im-placeholder-title">&chat.noAccount.title;</label>
+                        <description id="noAccountDesc"
+                                     class="im-placeholder-desc">&chat.noAccount.description;</description>
+                        <hbox class="im-placeholder-button-box" flex="1">
+                          <spacer flex="1"/>
+                          <button id="openIMAccountWizardButton" label="&chat.accountWizard.button;"
+                                  oncommand="openIMAccountWizard();"/>
+                        </hbox>
+                      </vbox>
+                      <vbox id="noConnectedAccountInnerBox" class="im-placeholder-innerbox" flex="1" hidden="true">
+                        <label id="noConnectedAccountTitle"
+                              class="im-placeholder-title">&chat.noConnectedAccount.title;</label>
+                        <description id="noConnectedAccountDesc"
+                                     class="im-placeholder-desc">&chat.noConnectedAccount.description;</description>
+                        <hbox class="im-placeholder-button-box" flex="1">
+                          <spacer flex="1"/>
+                          <button id="openIMAccountManagerButton" label="&chat.showAccountManager.button;"
+                                  oncommand="openIMAccountMgr();"/>
+                        </hbox>
+                      </vbox>
+                    </hbox>
+                  </vbox>
+
+                  <vbox id="logDisplay" flex="1">
+                    <deck id="logDisplayDeck" flex="1">
+                      <vbox flex="1" id="noPreviousConvScreen" class="im-placeholder-screen" align="center" pack="center">
+                        <hbox id="noPreviousConvBox" class="im-placeholder-box" align="top">
+                          <vbox id="noPreviousConvInnerBox" class="im-placeholder-innerbox" flex="1">
+                            <description id="noPreviousConvDesc"
+                                         class="im-placeholder-desc">&chat.noPreviousConv.description;</description>
+                          </vbox>
+                        </hbox>
+                      </vbox>
+                      <vbox flex="1" id="logDisplayBrowserBox">
+                        <browser id="conv-log-browser" is="conversation-browser" type="content"
+                                contextmenu="chatConversationContextMenu" flex="1"
+                                tooltip="imTooltip"/>
+                        <html:progress id="log-browserProgress" max="100" hidden="true"/>
+                        <findbar id="log-findbar" browserid="conv-log-browser"/>
+                      </vbox>
+                    </deck>
+                    <button id="goToConversation" hidden="true"
+                            oncommand="chatHandler.showCurrentConversation();"/>
+                  </vbox>
+
+                </deck>
+              </vbox>
               <splitter id="contextSplitter" hidden="true" collapse="after"/>
               <vbox id="contextPane" hidden="true" width="250" persist="width">
                 <chat-conversation-info id="conv-top-info" class="conv-top-info"/>
                 <vbox id="contextPaneFlexibleBox" flex="1">
                   <vbox flex="1" class="conv-chat" width="150">
                     <hbox align="baseline" class="conv-nicklist-header">
                       <label class="conv-nicklist-header-label"
                              id="participantLabel" control="participantCount"
--- a/mail/components/im/content/chat-messenger.js
+++ b/mail/components/im/content/chat-messenger.js
@@ -8,17 +8,19 @@
 /* globals fixIterator, MailToolboxCustomizeDone, Notifications, openIMAccountMgr,
    PROTO_TREE_VIEW, Services, Status, statusSelector, ZoomManager */
 
 var {Notifications} = ChromeUtils.import("resource:///modules/chatNotifications.jsm");
 var { Services: imServices } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(this, "OTRUI", "resource:///modules/OTRUI.jsm");
 
+var gOtrEnabled = false;
 var gBuddyListContextMenu = null;
 
 function buddyListContextMenu(aXulMenu) {
   this.target = aXulMenu.triggerNode;
   this.menu = aXulMenu;
   let localName = this.target.localName;
   this.onContact = (localName == "richlistitem" &&
     this.target.getAttribute("is") == "chat-contact");
@@ -30,17 +32,22 @@ function buddyListContextMenu(aXulMenu) 
   ["context-openconversation", "context-edit-buddy-separator",
     "context-alias", "context-delete"].forEach(function(aId) {
     document.getElementById(aId).hidden = hide;
   });
 
   document.getElementById("context-close-conversation").hidden = !this.onConv;
   document.getElementById("context-openconversation").disabled =
     !hide && !this.target.canOpenConversation();
+
+  if (gOtrEnabled) {
+    OTRUI.addBuddyContextMenu(this.menu, document);
+  }
 }
+
 buddyListContextMenu.prototype = {
   openConversation() {
     if (this.onContact || this.onConv)
       this.target.openConversation();
   },
   closeConversation() {
     if (this.onConv)
       this.target.closeConversation();
@@ -624,16 +631,19 @@ var chatHandler = {
     let item = contactlistbox.selectedItem;
     if (!item || item.hidden ||
         (item.localName == "richlistitem" && item.getAttribute("is") == "chat-group")) {
       this._hideContextPane(true);
       document.getElementById("conversationsDeck").selectedPanel =
         document.getElementById("noConvScreen");
       this.updateTitle();
       this.observedContact = null;
+      if (gOtrEnabled) {
+        OTRUI.hideOTRButton();
+      }
       return;
     }
 
     this._hideContextPane(false);
 
     if (item.getAttribute("id") == "searchResultConv") {
       document.getElementById("goToConversation").hidden = true;
       document.getElementById("contextPane").removeAttribute("chat");
@@ -643,16 +653,19 @@ var chatHandler = {
       cti.removeAttribute("statusMessageWithDash");
       cti.removeAttribute("statusMessage");
       cti.removeAttribute("status");
       cti.removeAttribute("statusTypeTooltiptext");
       cti.removeAttribute("statusTooltiptext");
       cti.removeAttribute("topicEditable");
       cti.removeAttribute("noTopic");
       this.observedContact = null;
+      if (gOtrEnabled) {
+        OTRUI.hideOTRButton();
+      }
 
       let path = "logs/" + item.log.path;
       path = OS.Path.join(OS.Constants.Path.profileDir, ...path.split("/"));
       imServices.logs.getLogFromFile(path, true).then(aLog => {
         imServices.logs.getSimilarLogs(aLog, true).then(aSimilarLogs => {
           if (contactlistbox.selectedItem != item)
             return;
           this._pendingSearchTerm = item.searchTerm || undefined;
@@ -675,16 +688,20 @@ var chatHandler = {
       } else {
         item.convView.onConvResize();
       }
 
       convDeck.selectedPanel = item.convView;
       item.convView.updateConvStatus();
       item.update();
 
+      if (gOtrEnabled) {
+        OTRUI.updateOTRButton(item.conv);
+      }
+
       imServices.logs.getLogsForConversation(item.conv, true).then(aLogs => {
         if (contactlistbox.selectedItem != item)
           return;
         this._showLogList(aLogs);
       });
 
       let contextPane = document.getElementById("contextPane");
       if (item.conv.isChat) {
@@ -695,16 +712,19 @@ var chatHandler = {
       }
 
       let button = document.getElementById("goToConversation");
       let bundle = document.getElementById("chatBundle");
       button.label = bundle.getString("goBackToCurrentConversation.button");
       button.disabled = false;
       this.observedContact = null;
     } else if (item.localName == "richlistitem" && item.getAttribute("is") == "chat-contact") {
+      if (gOtrEnabled) {
+        OTRUI.hideOTRButton();
+      }
       let contact = item.contact;
       if (this.observedContact && contact &&
           this.observedContact.id == contact.id) {
         return; // onselect has just been fired again because a status
                 // change caused the chat-contact to move.
                 // Return early to avoid flickering and changing the selected log.
       }
 
@@ -1209,16 +1229,38 @@ var chatHandler = {
 
     ChromeUtils.import("resource:///modules/chatHandler.jsm", this);
     if (this.ChatCore.initialized) {
       this.initAfterChatCore();
     } else {
       this.ChatCore.init();
       this._addObserver("chat-core-initialized");
     }
+
+    gOtrEnabled =
+      Services.prefs.getBoolPref("chat.otr.enable");
+
+    if (gOtrEnabled) {
+      new Promise(resolve => {
+        if (Services.core.initialized) {
+          resolve();
+          return;
+        }
+        function initObserver() {
+          Services.obs.removeObserver(initObserver, "prpl-init");
+          resolve();
+        }
+        Services.obs.addObserver(initObserver, "prpl-init");
+      }).then(() => {
+        let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
+        let uri = Services.io.newURI("chrome://chat/skin/otr.css");
+        sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
+        OTRUI.init();
+      });
+    }
   },
 };
 
 function chatLogTreeGroupItem(aTitle, aLogItems) {
   this._title = aTitle;
   this._children = aLogItems;
   for (let child of this._children)
     child._parent = this;
--- a/mail/components/im/themes/chat.css
+++ b/mail/components/im/themes/chat.css
@@ -287,17 +287,16 @@ richlistitem[is="chat-imconv"]:not(:hove
 }
 
 .conv-top {
   min-height: 60px;
 }
 
 .conv-top-info {
   margin: 0;
-  padding: 0.6ex;
   border-style: none;
   -moz-appearance: none;
   -moz-window-dragging: no-drag;
   border-bottom: 1px solid var(--splitter-color);
 }
 
 .userIconHolder {
   border: 2px solid rgba(0,0,0,0.15);
@@ -475,16 +474,21 @@ richlistitem[state="disconnected"] .acco
   list-style-image: url('chrome://chat/skin/offline.png');
 }
 
 .status-overlay-icon[status="unknown"] {
   list-style-image: url('chrome://chat/skin/unknown.png');
 }
 
 /* corresponds to im/themes/conversation.css @media all and (min-height: 251px) */
+.displayUserAccount {
+  padding: 4px;
+  background-color: -moz-OddTreeRow;
+}
+
 .statusImageStack,
 .displayNameAndstatusMessageStack {
   margin: 2px 2px;
 }
 
 .displayName {
   font-size: 16px;
   border-bottom: 1px solid var(--splitter-color);