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 cba7eb3d7e..c6a1edb57b 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 7ea2b49cdb..a12b028e95 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 Stian Thorgersen */ public interface EmailSenderProvider extends Provider { - void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException; - + void send(Map 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 1cc6151d6b..da245fcd76 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 Stian Thorgersen */ @@ -46,6 +48,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 config, UserModel user) throws EmailException; + /** * Send to confirm that user wants to link his account with identity broker link */ diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java index 7477d843fa..ca3575c7a9 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 config, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { Transport transport = null; try { String address = retrieveEmailAddress(user); - Map 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 5105eaef41..abc23a1f31 100755 --- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java +++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java @@ -107,6 +107,19 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { send("passwordResetSubject", "password-reset.ftl", attributes); } + @Override + public void sendSmtpTestEmail(Map config, UserModel user) throws EmailException { + setRealm(session.getContext().getRealm()); + setUser(user); + + Map attributes = new HashMap(); + 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 attributes = new HashMap(); @@ -156,7 +169,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { send(subjectKey, Collections.emptyList(), template, attributes); } - private void send(String subjectKey, List subjectAttributes, String template, Map attributes) throws EmailException { + private EmailTemplate processTemplate(String subjectKey, List subjectAttributes, String template, Map 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 subjectAttributes, String template, Map 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 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 28392f7f87..ebc89bea62 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 settings = readValue(config, new TypeReference>() { + }); + + 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 bc0b7873a5..7ebaa1da57 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 0000000000..303cfd65ad --- /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 Bruno Oliveira + */ +public class SMTPConnectionTest extends AbstractKeycloakTest { + + @Rule + public GreenMailRule greenMailRule = new GreenMailRule(); + private RealmResource realm; + + @Override + public void addTestRealms(List testRealms) { + } + + @Before + public void before() { + realm = adminClient.realm("master"); + List 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 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 ab4e72a878..14eb89c523 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 e850b3bc4b..3170052646 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 5d3c68eedb..43df76147c 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 @@
+
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 0000000000..604415d22a --- /dev/null +++ b/themes/src/main/resources/theme/base/email/html/email-test.ftl @@ -0,0 +1,5 @@ + + +${msg("emailTestBodyHtml",realmName)} + + 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 9281bb7f7b..8a0ae92b95 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=

Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address

Link to e-mail address verification

This link will expire within {1} minutes.

If you didn''t create this account, just ignore this message.

+emailTestSubject=[KEYCLOAK] - SMTP test message +emailTestBody=This is a test message +emailTestBodyHtml=

This is a test message

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=

Someone wants to link your {1} account with {0} account of user {2} . If this was you, click the link below to link accounts

Link to confirm account linking

This link will expire within {4} minutes.

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}.

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 0000000000..19942c791f --- /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