keycloak-uncached
Changes
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 7(+7 -0)
services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java 64(+57 -7)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java 4(+4 -0)
Details
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index cba7eb3..c6a1edb 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -38,6 +38,7 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
@@ -184,6 +185,12 @@ public interface RealmResource {
                                 @QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential,
                                 @QueryParam("useTruststoreSpi") String useTruststoreSpi, @QueryParam("connectionTimeout") String connectionTimeout);
 
+    @Path("testSMTPConnection/{config}")
+    @POST
+    @NoCache
+    @Consumes(MediaType.APPLICATION_JSON)
+    Response testSMTPConnection(final @PathParam("config") String config) throws Exception;
+
     @Path("clear-realm-cache")
     @POST
     void clearRealmCache();
                diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
index 7ea2b49..a12b028 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailSenderProvider.java
@@ -17,15 +17,15 @@
 
 package org.keycloak.email;
 
-import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.provider.Provider;
 
+import java.util.Map;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public interface EmailSenderProvider extends Provider {
 
-    void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
-
+    void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
 }
                diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
index 1cc6151..da245fc 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
@@ -22,6 +22,8 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.provider.Provider;
 
+import java.util.Map;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -47,6 +49,15 @@ public interface EmailTemplateProvider extends Provider {
     public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
 
     /**
+     * Test SMTP connection with current logged in user
+     *
+     * @param config SMTP server configuration
+     * @param user SMTP recipient
+     * @throws EmailException
+     */
+    public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException;
+
+    /**
      * Send to confirm that user wants to link his account with identity broker link
      */
     void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException;
                diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
index 7477d84..ca3575c 100644
--- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
@@ -20,7 +20,6 @@ package org.keycloak.email;
 import com.sun.mail.smtp.SMTPMessage;
 import org.jboss.logging.Logger;
 import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.services.ServicesLogger;
 import org.keycloak.truststore.HostnameVerificationPolicy;
@@ -57,20 +56,22 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
     }
 
     @Override
-    public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
+    public void send(Map<String, String> config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
         Transport transport = null;
         try {
             String address = retrieveEmailAddress(user);
-            Map<String, String> config = realm.getSmtpConfig();
 
             Properties props = new Properties();
-            props.setProperty("mail.smtp.host", config.get("host"));
+
+            if (config.containsKey("host")) {
+                props.setProperty("mail.smtp.host", config.get("host"));
+            }
 
             boolean auth = "true".equals(config.get("auth"));
             boolean ssl = "true".equals(config.get("ssl"));
             boolean starttls = "true".equals(config.get("starttls"));
 
-            if (config.containsKey("port")) {
+            if (config.containsKey("port") && config.get("port") != null) {
                 props.setProperty("mail.smtp.port", config.get("port"));
             }
 
@@ -103,13 +104,13 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
 
             Multipart multipart = new MimeMultipart("alternative");
 
-            if(textBody != null) {
+            if (textBody != null) {
                 MimeBodyPart textPart = new MimeBodyPart();
                 textPart.setText(textBody, "UTF-8");
                 multipart.addBodyPart(textPart);
             }
 
-            if(htmlBody != null) {
+            if (htmlBody != null) {
                 MimeBodyPart htmlPart = new MimeBodyPart();
                 htmlPart.setContent(htmlBody, "text/html; charset=UTF-8");
                 multipart.addBodyPart(htmlPart);
@@ -153,13 +154,16 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
         }
     }
 
-    protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException {
+    protected InternetAddress toInternetAddress(String email, String displayName) throws UnsupportedEncodingException, AddressException, EmailException {
+        if (email == null || "".equals(email.trim())) {
+            throw new EmailException("Please provide a valid address", null);
+        }
         if (displayName == null || "".equals(displayName.trim())) {
             return new InternetAddress(email);
         }
         return new InternetAddress(email, displayName, "utf-8");
     }
-    
+
     protected String retrieveEmailAddress(UserModel user) {
         return user.getEmail();
     }
                diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
index 5105eae..abc23a1 100755
--- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
+++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
@@ -108,6 +108,19 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
     }
 
     @Override
+    public void sendSmtpTestEmail(Map<String, String> config, UserModel user) throws EmailException {
+        setRealm(session.getContext().getRealm());
+        setUser(user);
+
+        Map<String, Object> attributes = new HashMap<String, Object>();
+        attributes.put("user", new ProfileBean(user));
+        attributes.put("realmName", realm.getName());
+
+        EmailTemplate email = processTemplate("emailTestSubject", Collections.emptyList(), "email-test.ftl", attributes);
+        send(config, email.getSubject(), email.getTextBody(), email.getHtmlBody());
+    }
+
+    @Override
     public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException {
         Map<String, Object> attributes = new HashMap<String, Object>();
         attributes.put("user", new ProfileBean(user));
@@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
         send(subjectKey, Collections.emptyList(), template, attributes);
     }
 
-    private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+    private EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
         try {
             ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
             Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
@@ -168,27 +181,39 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
             String textTemplate = String.format("text/%s", template);
             String textBody;
             try {
-            	textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
+                textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
             } catch (final FreeMarkerException e ) {
-            	textBody = null;
+                textBody = null;
             }
             String htmlTemplate = String.format("html/%s", template);
             String htmlBody;
             try {
-            	htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
+                htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
             } catch (final FreeMarkerException e ) {
-            	htmlBody = null;
+                htmlBody = null;
             }
 
-            send(subject, textBody, htmlBody);
+            return new EmailTemplate(subject, textBody, htmlBody);
+        } catch (Exception e) {
+            throw new EmailException("Failed to template email", e);
+        }
+    }
+    private void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+        try {
+            EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
+            send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
         } catch (Exception e) {
             throw new EmailException("Failed to template email", e);
         }
     }
 
     private void send(String subject, String textBody, String htmlBody) throws EmailException {
+        send(realm.getSmtpConfig(), subject, textBody, htmlBody);
+    }
+
+    private void send(Map<String, String> config, String subject, String textBody, String htmlBody) throws EmailException {
         EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
-        emailSender.send(realm, user, subject, textBody, htmlBody);
+        emailSender.send(config, user, subject, textBody, htmlBody);
     }
 
     @Override
@@ -203,4 +228,29 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
         return sb.toString();
     }
 
+    private class EmailTemplate {
+
+        private String subject;
+        private String textBody;
+        private String htmlBody;
+
+        public EmailTemplate(String subject, String textBody, String htmlBody) {
+            this.subject = subject;
+            this.textBody = textBody;
+            this.htmlBody = htmlBody;
+        }
+
+        public String getSubject() {
+            return subject;
+        }
+
+        public String getTextBody() {
+            return textBody;
+        }
+
+        public String getHtmlBody() {
+            return htmlBody;
+        }
+    }
+
 }
                diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 28392f7..0a40c36 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -16,6 +16,7 @@
  */
 package org.keycloak.services.resources.admin;
 
+import com.fasterxml.jackson.core.type.TypeReference;
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.BadRequestException;
@@ -29,6 +30,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.common.VerificationException;
 import org.keycloak.common.util.PemUtils;
+import org.keycloak.email.EmailTemplateProvider;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventQuery;
 import org.keycloak.events.EventStoreProvider;
@@ -50,6 +52,7 @@ import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelDuplicateException;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.cache.CacheRealmProvider;
 import org.keycloak.models.cache.UserCache;
@@ -102,9 +105,9 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.PatternSyntaxException;
 
 import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
+import static org.keycloak.util.JsonSerialization.readValue;
 
 /**
  * Base resource class for the admin REST api of one realm
@@ -811,6 +814,35 @@ public class RealmAdminResource {
         return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
     }
 
+    /**
+     * Test SMTP connection with current logged in user
+     *
+     * @param config SMTP server configuration
+     * @return
+     * @throws Exception
+     */
+    @Path("testSMTPConnection/{config}")
+    @POST
+    @NoCache
+    public Response testSMTPConnection(final @PathParam("config") String config) throws Exception {
+        Map<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
+        });
+
+        try {
+            UserModel user = auth.getAuth().getUser();
+            if (user.getEmail() == null) {
+                return ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR);
+            }
+            session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user);
+        } catch (Exception e) {
+            e.printStackTrace();
+            logger.errorf("Failed to send email \n %s", e.getCause());
+            return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
+        }
+
+        return Response.noContent().build();
+    }
+
     @Path("identity-provider")
     public IdentityProvidersResource getIdentityProviderResource() {
         return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
                diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
index bc0b787..7ebaa1d 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
@@ -43,6 +43,10 @@ public class GreenMailRule extends ExternalResource {
         greenMail.start();
     }
 
+    public void credentials(String username, String password) {
+        greenMail.setUser(username, password);
+    }
+
     @Override
     protected void after() {
         if (greenMail != null) {
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java
new file mode 100644
index 0000000..303cfd6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/SMTPConnectionTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.admin;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.mail.internet.MimeMessage;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.keycloak.util.JsonSerialization.writeValueAsPrettyString;
+
+/**
+ * @author <a href="mailto:bruno@abstractj.org">Bruno Oliveira</a>
+ */
+public class SMTPConnectionTest extends AbstractKeycloakTest {
+
+    @Rule
+    public GreenMailRule greenMailRule = new GreenMailRule();
+    private RealmResource realm;
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+    }
+
+    @Before
+    public void before() {
+        realm = adminClient.realm("master");
+        List<UserRepresentation> admin = realm.users().search("admin", 0, 1);
+        UserRepresentation user = UserBuilder.edit(admin.get(0)).email("admin@localhost").build();
+        realm.users().get(user.getId()).update(user);
+    }
+
+    private String settings(String host, String port, String from, String auth, String ssl, String starttls,
+                            String username, String password) throws Exception {
+        Map<String, String> config = new HashMap<>();
+        config.put("host", host);
+        config.put("port", port);
+        config.put("from", from);
+        config.put("auth", auth);
+        config.put("ssl", ssl);
+        config.put("starttls", starttls);
+        config.put("user", username);
+        config.put("password", password);
+        return writeValueAsPrettyString(config);
+    }
+
+    @Test
+    public void testWithEmptySettings() throws Exception {
+        Response response = realm.testSMTPConnection(settings(null, null, null, null, null, null,
+                null, null));
+        assertStatus(response, 500);
+    }
+
+    @Test
+    public void testWithProperSettings() throws Exception {
+        Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", null, null, null,
+                null, null));
+        assertStatus(response, 204);
+        assertMailReceived();
+    }
+
+    @Test
+    public void testWithAuthEnabledCredentialsEmpty() throws Exception {
+        Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
+                null, null));
+        assertStatus(response, 500);
+    }
+
+    @Test
+    public void testWithAuthEnabledValidCredentials() throws Exception {
+        greenMailRule.credentials("admin@localhost", "admin");
+        Response response = realm.testSMTPConnection(settings("127.0.0.1", "3025", "auto@keycloak.org", "true", null, null,
+                "admin@localhost", "admin"));
+        assertStatus(response, 204);
+    }
+
+    private void assertStatus(Response response, int status) {
+        assertEquals(status, response.getStatus());
+        response.close();
+    }
+
+    private void assertMailReceived() {
+        if (greenMailRule.getReceivedMessages().length == 1) {
+            try {
+                MimeMessage message = greenMailRule.getReceivedMessages()[0];
+                assertEquals("[KEYCLOAK] - SMTP test message", message.getSubject());
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        } else {
+            fail("E-mail was not received");
+        }
+    }
+}
                diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index ab4e72a..14eb89c 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1449,7 +1449,7 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients
         $http, $location, Notifications, Dialog);
 });
 
-module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) {
+module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications, RealmSMTPConnectionTester) {
     console.log('RealmSMTPSettingsCtrl');
 
     var booleanSmtpAtts = ["auth","ssl","starttls"];
@@ -1484,6 +1484,25 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
         $scope.changed = false;
     };
 
+    var initSMTPTest = function() {
+        return {
+            realm: $scope.realm.realm,
+            config: JSON.stringify(realm.smtpServer)
+        };
+    };
+
+    $scope.testConnection = function() {
+        RealmSMTPConnectionTester.send(initSMTPTest(), function() {
+            Notifications.success("SMTP connection successful. E-mail was sent!");
+        }, function(errorResponse) {
+            if (error.data.errorMessage) {
+                Notifications.error(error.data.errorMessage);
+            } else {
+                Notifications.error('Unexpected error during SMTP validation');
+            }
+        });
+    };
+
     /* Convert string attributes containing a boolean to actual boolean type + convert an integer string (port) to integer. */
     function typeObject(obj){
         for (var att in obj){
                diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index e850b3b..3170052 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -320,6 +320,17 @@ module.factory('RealmLDAPConnectionTester', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
 });
 
+module.factory('RealmSMTPConnectionTester', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/testSMTPConnection/:config', {
+        realm : '@realm',
+        config : '@config'
+    }, {
+       send: {
+           method: 'POST'
+       }
+    });
+});
+
 module.service('ServerInfo', function($resource, $q, $http) {
     var info = {};
     var delay = $q.defer();
                diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
index 5d3c68e..43df761 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
@@ -10,6 +10,9 @@
             <div class="col-md-6">
                 <input class="form-control" id="smtpHost" type="text" ng-model="realm.smtpServer.host" placeholder="{{:: 'smtp-host' | translate}}" required>
             </div>
+            <div class="col-sm-4">
+                <a class="btn btn-primary" data-ng-click="testConnection()">{{:: 'test-connection' | translate}}</a>
+            </div>
         </div>
         <div class="form-group clearfix">
             <label class="col-md-2 control-label" for="smtpPort">{{:: 'port' | translate}}</label>
                diff --git a/themes/src/main/resources/theme/base/email/html/email-test.ftl b/themes/src/main/resources/theme/base/email/html/email-test.ftl
new file mode 100644
index 0000000..604415d
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/html/email-test.ftl
@@ -0,0 +1,5 @@
+<html>
+<body>
+${msg("emailTestBodyHtml",realmName)}
+</body>
+</html>
                diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
index 9281bb7..8a0ae92 100755
--- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
@@ -1,6 +1,9 @@
 emailVerificationSubject=Verify email
 emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
 emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
+emailTestSubject=[KEYCLOAK] - SMTP test message
+emailTestBody=This is a test message
+emailTestBodyHtml=<p>This is a test message</p>
 identityProviderLinkSubject=Link {0}
 identityProviderLinkBody=Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {4} minutes.\n\nIf you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.
 identityProviderLinkBodyHtml=<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">Link to confirm account linking</a></p><p>This link will expire within {4} minutes.</p><p>If you don''t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>
                diff --git a/themes/src/main/resources/theme/base/email/text/email-test.ftl b/themes/src/main/resources/theme/base/email/text/email-test.ftl
new file mode 100644
index 0000000..19942c7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/text/email-test.ftl
@@ -0,0 +1 @@
+${msg("emailTestBody", realmName)}
\ No newline at end of file