From 6e261425d813958d8ac9f927e29f0a1044a093c7 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Mon, 5 Oct 2026 15:46:34 +0300 Subject: [PATCH 1/2] Add starttls.required to make STARTTLS mandatory for external email STARTTLS was opportunistic: when the server's EHLO reply did not offer it, JavaMail went on in clear, so an on-path attacker who stripped STARTTLS got the SMTP credentials and the message. starttls.required maps to mail.smtp.starttls.required and fails the send instead. It defaults to false to keep servers without STARTTLS working, and implies enable, because JavaMail issues STARTTLS for required alone and the trust settings must apply then too. The Admin UI gets a "Require STARTTLS" switch, the samples set it, and chap-mail.adoc documents it. Fixes #246 --- .../asciidoc/integrators-guide/chap-mail.adoc | 7 +- openidm-external-email/pom.xml | 7 + .../external/email/impl/EmailClient.java | 13 +- .../external/email/impl/EmailClientTest.java | 148 +++++++++++++++++- .../ui/admin/settings/EmailConfigView.js | 13 ++ .../admin/settings/EmailConfigTemplate.html | 12 ++ .../ui/admin/settings/EmailConfigViewTest.js | 42 +++++ .../resources/locales/en/translation.json | 1 + .../samples/misc/external.email.json | 3 +- .../samples/usecase/conf/external.email.json | 3 +- 10 files changed, 243 insertions(+), 6 deletions(-) diff --git a/openidm-doc/src/main/asciidoc/integrators-guide/chap-mail.adoc b/openidm-doc/src/main/asciidoc/integrators-guide/chap-mail.adoc index 2df63146d9..f289c1d77c 100644 --- a/openidm-doc/src/main/asciidoc/integrators-guide/chap-mail.adoc +++ b/openidm-doc/src/main/asciidoc/integrators-guide/chap-mail.adoc @@ -64,7 +64,8 @@ $ cp samples/misc/external.email.json conf/ "writetimeout" : 300000, "connectiontimeout" : 300000, "starttls" : { - "enable" : true + "enable" : true, + "required" : true }, "ssl" : { "enable" : false @@ -122,7 +123,9 @@ If `"enable" : false`, you can leave the entries for `"username"` and `"password `starttls`:: -If `"enable" : true`, enables the use of the STARTTLS command (if supported by the server) to switch the connection to a TLS-protected connection before issuing any login commands. If the server does not support STARTTLS, the connection continues without the use of TLS. +If `"enable" : true`, enables the use of the STARTTLS command (if supported by the server) to switch the connection to a TLS-protected connection before issuing any login commands. If the server does not support STARTTLS, the connection continues without the use of TLS, unless `required` is set. ++ +Set `"required" : true` to refuse to send when the server does not offer STARTTLS; `required` implies `enable`. With the default, `false`, an attacker on the network path can remove STARTTLS from the server's reply and so receive the SMTP credentials and the message in clear. Leave it `false` only for a server that does not support STARTTLS. + The SMTP server certificate is validated against the JVM trust store and must be issued for the configured `host`. On Java 17 and later the host name is matched only against the certificate's DNS subject alternative names (or, if it has none, its CN), so `host` must be a DNS name the certificate carries; a relay addressed by IP address is rejected even if its certificate has an IP address subject alternative name. Two optional settings relax that: + diff --git a/openidm-external-email/pom.xml b/openidm-external-email/pom.xml index 8992d23e78..834b145b6a 100644 --- a/openidm-external-email/pom.xml +++ b/openidm-external-email/pom.xml @@ -108,6 +108,13 @@ mockito-all test + + + com.sun.activation + javax.activation + 1.2.0 + test + org.openidentityplatform.commons diff --git a/openidm-external-email/src/main/java/org/forgerock/openidm/external/email/impl/EmailClient.java b/openidm-external-email/src/main/java/org/forgerock/openidm/external/email/impl/EmailClient.java index 52e15a9bc2..408331cedb 100644 --- a/openidm-external-email/src/main/java/org/forgerock/openidm/external/email/impl/EmailClient.java +++ b/openidm-external-email/src/main/java/org/forgerock/openidm/external/email/impl/EmailClient.java @@ -71,6 +71,11 @@ public class EmailClient { public static final String CONFIG_MAIL_SMTP_AUTH_USERNAME = "username"; public static final String CONFIG_MAIL_SMTP_STARTTLS = "starttls"; public static final String CONFIG_MAIL_SMTP_STARTTLS_ENABLE = "enable"; + /** + * Fail instead of sending in clear when the server does not offer STARTTLS. Implies + * {@code enable}. Off by default: STARTTLS is then used only if the server offers it. + */ + public static final String CONFIG_MAIL_SMTP_STARTTLS_REQUIRED = "required"; /** Opt-in: accept any server certificate over STARTTLS. Never use outside development. */ public static final String CONFIG_MAIL_SMTP_STARTTLS_TRUST_ALL = "trustAll"; /** @@ -98,9 +103,15 @@ public EmailClient(JsonValue config) throws RuntimeException { } JsonValue starttlsConfig = config.get(CONFIG_MAIL_SMTP_STARTTLS); - boolean startTLS = starttlsConfig.get(CONFIG_MAIL_SMTP_STARTTLS_ENABLE).defaultTo(false).asBoolean(); + boolean startTLSRequired = + starttlsConfig.get(CONFIG_MAIL_SMTP_STARTTLS_REQUIRED).defaultTo(false).asBoolean(); + // JavaMail issues STARTTLS for "required" alone, so it implies "enable" and the trust settings below + boolean startTLS = startTLSRequired + || starttlsConfig.get(CONFIG_MAIL_SMTP_STARTTLS_ENABLE).defaultTo(false).asBoolean(); if (startTLS) { props.put("mail.smtp.starttls.enable", String.valueOf(startTLS)); + // when true, fail instead of continuing in clear if the server does not offer STARTTLS + props.put("mail.smtp.starttls.required", String.valueOf(startTLSRequired)); // without this JavaMail 1.4.7 enables only TLSv1 for STARTTLS, which current JDKs disable props.put("mail.smtp.ssl.protocols", defaultTlsProtocols()); configureStartTlsTrust(starttlsConfig); diff --git a/openidm-external-email/src/test/java/org/forgerock/openidm/external/email/impl/EmailClientTest.java b/openidm-external-email/src/test/java/org/forgerock/openidm/external/email/impl/EmailClientTest.java index 628ff368d2..ae042bc9e0 100644 --- a/openidm-external-email/src/test/java/org/forgerock/openidm/external/email/impl/EmailClientTest.java +++ b/openidm-external-email/src/test/java/org/forgerock/openidm/external/email/impl/EmailClientTest.java @@ -16,28 +16,42 @@ package org.forgerock.openidm.external.email.impl; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.forgerock.json.JsonValue.array; import static org.forgerock.json.JsonValue.field; import static org.forgerock.json.JsonValue.json; import static org.forgerock.json.JsonValue.object; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; import javax.mail.Session; import javax.net.ssl.SSLContext; import com.sun.mail.util.MailSSLSocketFactory; import org.forgerock.json.JsonValue; +import org.forgerock.json.resource.BadRequestException; import org.testng.annotations.Test; /** - * Tests for the STARTTLS trust settings of {@link EmailClient}. + * Tests for the STARTTLS settings of {@link EmailClient}. */ public class EmailClientTest { private static final String SOCKET_FACTORY = "mail.smtp.ssl.socketFactory"; private static final String CHECK_SERVER_IDENTITY = "mail.smtp.ssl.checkserveridentity"; + private static final String STARTTLS_REQUIRED = "mail.smtp.starttls.required"; @Test public void startTlsValidatesTheServerCertificateByDefault() throws Exception { @@ -105,6 +119,61 @@ public void socketFactoryComesFromTheJavaMailInUse() { .isEqualTo(Session.class.getProtectionDomain().getCodeSource().getLocation()); } + @Test + public void startTlsIsOpportunisticByDefault() throws Exception { + Properties props = sessionProperties(json(object( + field("host", "smtp.example.com"), + field("starttls", object(field("enable", true)))))); + + assertThat(props.get(STARTTLS_REQUIRED)).isEqualTo("false"); + } + + @Test + public void startTlsRequiredRejectsServersWithoutStartTls() throws Exception { + Properties props = sessionProperties(json(object( + field("host", "smtp.example.com"), + field("starttls", object(field("enable", true), field("required", true)))))); + + assertThat(props.get(STARTTLS_REQUIRED)).isEqualTo("true"); + assertThat(props.get(CHECK_SERVER_IDENTITY)).isEqualTo("true"); + } + + @Test + public void startTlsRequiredImpliesStartTls() throws Exception { + Properties props = sessionProperties(json(object( + field("host", "smtp.example.com"), + field("starttls", object(field("enable", false), field("required", true)))))); + + // JavaMail issues STARTTLS for required alone, so the trust settings must apply as well + assertThat(props.get("mail.smtp.starttls.enable")).isEqualTo("true"); + assertThat(props.get(STARTTLS_REQUIRED)).isEqualTo("true"); + assertThat(props.get(CHECK_SERVER_IDENTITY)).isEqualTo("true"); + } + + @Test + public void startTlsRequiredStopsBeforeMailWhenTheServerDoesNotOfferIt() throws Exception { + try (StartTlsStrippingServer server = new StartTlsStrippingServer()) { + EmailClient client = new EmailClient(server.config(true)); + try { + client.send(message()); + fail("sent over a connection without STARTTLS"); + } catch (BadRequestException e) { + assertThat(e.getCause()).hasMessageContaining("STARTTLS is required"); + } + assertThat(server.commands()).as("connected and read the EHLO reply").anyMatch(c -> c.startsWith("EHLO")); + assertThat(server.commands()).noneMatch(c -> c.startsWith("MAIL FROM")); + } + } + + @Test + public void startTlsWithoutRequiredSendsInClearWhenTheServerDoesNotOfferIt() throws Exception { + try (StartTlsStrippingServer server = new StartTlsStrippingServer()) { + new EmailClient(server.config(false)).send(message()); + + assertThat(server.commands()).anyMatch(c -> c.startsWith("MAIL FROM")); + } + } + @Test public void trustSettingsApplyOnlyWithStartTls() throws Exception { Properties props = sessionProperties(json(object( @@ -116,6 +185,83 @@ public void trustSettingsApplyOnlyWithStartTls() throws Exception { assertThat(props.get(CHECK_SERVER_IDENTITY)).isNull(); } + private static JsonValue message() { + return json(object( + field("from", "idm@example.com"), + field("to", "user@example.com"), + field("subject", "test"), + field("body", "test"))); + } + + /** + * An SMTP server whose EHLO reply does not offer STARTTLS, as seen by a client whose + * connection is tampered with on path; it accepts every message in clear. + */ + private static final class StartTlsStrippingServer implements AutoCloseable { + + private final ServerSocket serverSocket; + private final List commands = new CopyOnWriteArrayList<>(); + private final Thread thread; + + StartTlsStrippingServer() throws IOException { + serverSocket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); + thread = new Thread(this::serve, "fake-smtp"); + thread.setDaemon(true); + thread.start(); + } + + JsonValue config(boolean required) { + return json(object( + field("host", serverSocket.getInetAddress().getHostAddress()), + field("port", String.valueOf(serverSocket.getLocalPort())), + field("starttls", object(field("enable", true), field("required", required))))); + } + + List commands() throws InterruptedException { + thread.join(10_000); + return commands; + } + + private void serve() { + try (Socket socket = serverSocket.accept(); + BufferedReader in = new BufferedReader( + new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)); + Writer out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII)) { + reply(out, "220 localhost ESMTP"); + String line; + while ((line = in.readLine()) != null) { + commands.add(line); + if (line.startsWith("EHLO")) { + reply(out, "250-localhost\r\n250 8BITMIME"); + } else if (line.equals("DATA")) { + reply(out, "354 end with ."); + while ((line = in.readLine()) != null && !line.equals(".")) { + // message content + } + reply(out, "250 OK"); + } else if (line.equals("QUIT")) { + reply(out, "221 bye"); + return; + } else { + reply(out, "250 OK"); + } + } + } catch (IOException e) { + // the client closed the connection + } + } + + private static void reply(Writer out, String reply) throws IOException { + out.write(reply + "\r\n"); + out.flush(); + } + + @Override + public void close() throws IOException { + serverSocket.close(); + } + } + private static Properties sessionProperties(JsonValue config) throws Exception { EmailClient client = new EmailClient(config); Field session = EmailClient.class.getDeclaredField("session"); diff --git a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/settings/EmailConfigView.js b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/settings/EmailConfigView.js index 29061ce6fb..2ac231e2a5 100644 --- a/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/settings/EmailConfigView.js +++ b/openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/settings/EmailConfigView.js @@ -37,6 +37,8 @@ define([ noBaseTemplate: true, events: { "click #emailAuth": "toggleUserPass", + "change #emailTLS": "syncStartTls", + "change #emailTLSRequired": "syncStartTls", "change #emailToggle": "toggleEmail", "change #emailAuthPassword": "updatePassword", "click #saveEmailConfig": "save" @@ -105,6 +107,17 @@ define([ this.$el.find("#smtpauth").slideToggle($(e.currentTarget).prop("checked")); }, + // "required" implies STARTTLS (see EmailClient), so keep the two switches consistent + syncStartTls: function(e) { + var checked = $(e.currentTarget).prop("checked"); + + if (e.currentTarget.id === "emailTLSRequired" && checked) { + this.$el.find("#emailTLS").prop("checked", true); + } else if (e.currentTarget.id === "emailTLS" && !checked) { + this.$el.find("#emailTLSRequired").prop("checked", false); + } + }, + toggleEmail: function() { if (!this.$el.find("#emailToggle").is(":checked")) { if (this.$el.find("#smtpauth").is(":visible")) { diff --git a/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html b/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html index 8f69540d23..4bdfa9a85f 100644 --- a/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html +++ b/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html @@ -1,4 +1,5 @@
@@ -60,6 +61,17 @@
+
+ +
+
+ +
+
+
+
diff --git a/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js b/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js index cf78712bd5..814e6fed0e 100644 --- a/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js +++ b/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js @@ -50,4 +50,46 @@ define([ assert.deepEqual(saved.starttls.trustedHosts, ["smtp.internal"], "trustedHosts is kept"); assert.strictEqual(saved.starttls.trustAll, false, "trustAll is kept"); }); + + QUnit.test("save stores an unchecked STARTTLS required switch as false", function (assert) { + var saved, + stub = sinon.stub(ConfigDelegate, "updateEntity", function (id, config) { + saved = config; + return $.Deferred(); + }); + + $("#qunit-fixture").html('' + + '' + + '' + + '' + + '' + + ''); + EmailConfigView.$el = $("#qunit-fixture"); + EmailConfigView.model = { externalEmailExists: true }; + EmailConfigView.data = { + config: { + host: "smtp.example.com", + starttls: { enable: true, required: true } + } + }; + + EmailConfigView.save({ preventDefault: $.noop }); + stub.restore(); + + assert.strictEqual(saved.starttls.required, false, "the form's value wins over the stored one"); + }); + + QUnit.test("STARTTLS required implies STARTTLS in the form", function (assert) { + $("#qunit-fixture").html('' + + ''); + EmailConfigView.$el = $("#qunit-fixture"); + + $("#emailTLSRequired").prop("checked", true); + EmailConfigView.syncStartTls({ currentTarget: $("#emailTLSRequired")[0] }); + assert.ok($("#emailTLS").prop("checked"), "checking required turns STARTTLS on"); + + $("#emailTLS").prop("checked", false); + EmailConfigView.syncStartTls({ currentTarget: $("#emailTLS")[0] }); + assert.notOk($("#emailTLSRequired").prop("checked"), "turning STARTTLS off clears required"); + }); }); diff --git a/openidm-ui/openidm-ui-common/src/main/resources/locales/en/translation.json b/openidm-ui/openidm-ui-common/src/main/resources/locales/en/translation.json index 39a0d65941..94314a4fc8 100644 --- a/openidm-ui/openidm-ui-common/src/main/resources/locales/en/translation.json +++ b/openidm-ui/openidm-ui-common/src/main/resources/locales/en/translation.json @@ -1493,6 +1493,7 @@ "password": "Password", "smtp": "Use SMTP Authentication", "tls": "Use STARTTLS", + "tlsRequired": "Require STARTTLS", "from": "Sender Email Address" }, "connector" : { diff --git a/openidm-zip/src/main/resources/samples/misc/external.email.json b/openidm-zip/src/main/resources/samples/misc/external.email.json index 2ecd1105b8..e86d3defe9 100644 --- a/openidm-zip/src/main/resources/samples/misc/external.email.json +++ b/openidm-zip/src/main/resources/samples/misc/external.email.json @@ -8,6 +8,7 @@ "password" : "xxxxxxxx" }, "starttls" : { - "enable" : true + "enable" : true, + "required" : true } } \ No newline at end of file diff --git a/openidm-zip/src/main/resources/samples/usecase/conf/external.email.json b/openidm-zip/src/main/resources/samples/usecase/conf/external.email.json index 2ecd1105b8..e86d3defe9 100644 --- a/openidm-zip/src/main/resources/samples/usecase/conf/external.email.json +++ b/openidm-zip/src/main/resources/samples/usecase/conf/external.email.json @@ -8,6 +8,7 @@ "password" : "xxxxxxxx" }, "starttls" : { - "enable" : true + "enable" : true, + "required" : true } } \ No newline at end of file From def91dd5a2c90238d1b73afdbdc0566ab2498d46 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Tue, 6 Oct 2026 10:33:09 +0300 Subject: [PATCH 2/2] Address review on starttls.required: render state and UI tests - EmailConfigTemplate: render "Use STARTTLS" on when only starttls.required is stored, since required implies enable in EmailClient - EmailConfigViewTest: render the real template, so the save case pins the Require switch's name and value="true", and drive both switches through change events, so the events map is covered --- .../admin/settings/EmailConfigTemplate.html | 2 +- .../ui/admin/settings/EmailConfigViewTest.js | 91 ++++++++++++------- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html b/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html index 4bdfa9a85f..fe83a0eb50 100644 --- a/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html +++ b/openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html @@ -55,7 +55,7 @@
diff --git a/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js b/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js index 814e6fed0e..1a8234c33d 100644 --- a/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js +++ b/openidm-ui/openidm-ui-admin/src/test/qunit/org/forgerock/openidm/ui/admin/settings/EmailConfigViewTest.js @@ -18,8 +18,10 @@ define([ "jquery", "sinon", "org/forgerock/openidm/ui/admin/settings/EmailConfigView", - "org/forgerock/openidm/ui/common/delegates/ConfigDelegate" -], function ($, sinon, EmailConfigView, ConfigDelegate) { + "org/forgerock/openidm/ui/common/delegates/ConfigDelegate", + "org/forgerock/openidm/ui/common/util/ThemeManager", + "org/forgerock/commons/ui/common/main/ValidatorsManager" +], function ($, sinon, EmailConfigView, ConfigDelegate, ThemeManager, ValidatorsManager) { QUnit.module('EmailConfigView Tests'); QUnit.test("save keeps the STARTTLS keys the form does not edit", function (assert) { @@ -51,45 +53,72 @@ define([ assert.strictEqual(saved.starttls.trustAll, false, "trustAll is kept"); }); - QUnit.test("save stores an unchecked STARTTLS required switch as false", function (assert) { - var saved, - stub = sinon.stub(ConfigDelegate, "updateEntity", function (id, config) { - saved = config; - return $.Deferred(); - }); + // renders the real template with the given stored config + function renderStored(config, callback) { + var theme = sinon.stub(ThemeManager, "getTheme", function () { + return $.Deferred().resolve({}); + }), + read = sinon.stub(ConfigDelegate, "readEntity", function () { + return $.Deferred().resolve(config); + }), + bind = sinon.stub(ValidatorsManager, "bindValidators"), + validate = sinon.stub(ValidatorsManager, "validateAllFields"); - $("#qunit-fixture").html('' + - '
' + - '' + - '' + - '' + - '
'); - EmailConfigView.$el = $("#qunit-fixture"); - EmailConfigView.model = { externalEmailExists: true }; - EmailConfigView.data = { - config: { - host: "smtp.example.com", - starttls: { enable: true, required: true } - } - }; + $("#qunit-fixture").html('
'); + EmailConfigView.model = { externalEmailExists: false }; + EmailConfigView.data = { config: {} }; + EmailConfigView.render([], function () { + theme.restore(); + read.restore(); + bind.restore(); + validate.restore(); + callback(); + EmailConfigView.undelegateEvents(); + }); + } - EmailConfigView.save({ preventDefault: $.noop }); - stub.restore(); + QUnit.test("a stored STARTTLS required renders Use STARTTLS on", function (assert) { + var done = assert.async(); - assert.strictEqual(saved.starttls.required, false, "the form's value wins over the stored one"); + renderStored({ host: "smtp.example.com", starttls: { required: true } }, function () { + assert.ok(EmailConfigView.$el.find("#emailTLS").prop("checked"), "required implies STARTTLS"); + assert.ok(EmailConfigView.$el.find("#emailTLSRequired").prop("checked"), "required is shown"); + done(); + }); }); - QUnit.test("STARTTLS required implies STARTTLS in the form", function (assert) { + QUnit.test("the rendered Require STARTTLS switch saves false when unchecked", function (assert) { + var done = assert.async(); + + renderStored({ host: "smtp.example.com", starttls: { enable: true, required: true } }, function () { + var saved, + update = sinon.stub(ConfigDelegate, "updateEntity", function (id, config) { + saved = config; + return $.Deferred(); + }); + + EmailConfigView.model.externalEmailExists = true; + EmailConfigView.$el.find("#emailTLSRequired").prop("checked", false); + EmailConfigView.save({ preventDefault: $.noop }); + update.restore(); + + assert.strictEqual(saved.starttls.enable, true, "STARTTLS stays on"); + assert.strictEqual(saved.starttls.required, false, "the template's switch overrides the stored true"); + done(); + }); + }); + + QUnit.test("the STARTTLS switches stay consistent on change", function (assert) { $("#qunit-fixture").html('' + ''); - EmailConfigView.$el = $("#qunit-fixture"); + EmailConfigView.setElement($("#qunit-fixture")); - $("#emailTLSRequired").prop("checked", true); - EmailConfigView.syncStartTls({ currentTarget: $("#emailTLSRequired")[0] }); + $("#emailTLSRequired").prop("checked", true).trigger("change"); assert.ok($("#emailTLS").prop("checked"), "checking required turns STARTTLS on"); - $("#emailTLS").prop("checked", false); - EmailConfigView.syncStartTls({ currentTarget: $("#emailTLS")[0] }); + $("#emailTLS").prop("checked", false).trigger("change"); assert.notOk($("#emailTLSRequired").prop("checked"), "turning STARTTLS off clears required"); + + EmailConfigView.undelegateEvents(); }); });