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..ebc89be 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.adminAuth().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