KEYCLOAK-681 multi-part emails

This commit is contained in:
Cory Snyder 2015-06-02 12:25:58 -04:00
parent 26acf6fa83
commit 0ac000adca
20 changed files with 183 additions and 43 deletions

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("emailVerificationBodyHtml",link, linkExpiration, realmName)}
</body>
</html>

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("eventLoginErrorBodyHtml",event.date,event.ipAddress)}
</body>
</html>

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("eventRemoveTotpBodyHtml",event.date, event.ipAddress)}
</body>
</html>

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("eventUpdatePasswordBodyHtml",event.date, event.ipAddress)}
</body>
</html>

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("eventUpdateTotpBodyHtml",event.date, event.ipAddress)}
</body>
</html>

View file

@ -0,0 +1,5 @@
<html>
<body>
${msg("passwordResetBodyHtml",link, linkExpiration, realmName)}
</body>
</html>

View file

@ -1,12 +1,18 @@
emailVerificationSubject=E-Mail verifizieren
passwordResetSubject=Passwort zur\u00FCckzusetzen
passwordResetBody=Jemand hat angefordert Ihr {2} Passwort zur\u00FCckzusetzen. Falls das Sie waren, dann klicken Sie auf den folgenden Link um das Passwort zur\u00FCckzusetzen.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie das Passwort nicht zur\u00FCcksetzen m\u00F6chten, dann k\u00F6nnen Sie diese E-Mail ignorieren.
passwordResetBodyHtml=<p>Jemand hat angefordert Ihr {2} Passwort zur\u00FCckzusetzen. Falls das Sie waren, dann klicken Sie auf den folgenden Link um das Passwort zur\u00FCckzusetzen.</p><p><a href="{0}">{0}</a></p><p>Dieser Link wird in {1} Minuten ablaufen.</p><p>Falls Sie das Passwort nicht zur\u00FCcksetzen m\u00F6chten, dann k\u00F6nnen Sie diese E-Mail ignorieren.</p>
emailVerificationBody=Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Fall das Sie waren, dann klicken Sie auf den Link um die E-Mail Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.
emailVerificationBodyHtml=<p>Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Fall das Sie waren, dann klicken Sie auf den Link um die E-Mail Adresse zu verifizieren.</p><p><a href="{0}">{0}</a></p><p>Dieser Link wird in {1} Minuten ablaufen.</p><p>Falls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.</p>
eventLoginErrorSubject=Fehlgeschlagene Anmeldung
eventLoginErrorBody=Jemand hat um {0} von {1} versucht sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.
eventLoginErrorBodyHtml=<p>Jemand hat um {0} von {1} versucht sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>
eventRemoveTotpSubject=TOTP Entfernt
eventRemoveTotpBody=TOTP wurde von ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.
eventRemoveTotpBodyHtml=<p>TOTP wurde von ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>
eventUpdatePasswordSubject=Passwort Aktualisiert
eventUpdatePasswordBody=Ihr Passwort wurde am {0} von {1} ge\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.
eventUpdatePasswordBodyHtml=<p>Ihr Passwort wurde am {0} von {1} ge\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>
eventUpdateTotpSubject=TOTP Aktualisiert
eventUpdateTotpBody=TOTP wurde am {0} von {1} ge\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.
eventUpdateTotpBodyHtml=<p>TOTP wurde am {0} von {1} ge\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>

View file

@ -1,12 +1,18 @@
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}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
passwordResetSubject=Reset password
passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>
eventLoginErrorSubject=Login error
eventLoginErrorBody=A failed login attempt was dettected to your account on {0} from {1}. If this was not you, please contact an admin.
eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.
eventLoginErrorBodyHtml=<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.</p>
eventRemoveTotpSubject=Remove TOTP
eventRemoveTotpBody=TOTP was removed from your account on {0} from {1}. If this was not you, please contact an admin.
eventRemoveTotpBodyHtml=<p>TOTP was removed from your account on {0} from {1}. If this was not you, please contact an admin.</p>
eventUpdatePasswordSubject=Update password
eventUpdatePasswordBody=Your password was changed on {0} from {1}. If this was not you, please contact an admin.
eventUpdatePasswordBodyHtml=<p>Your password was changed on {0} from {1}. If this was not you, please contact an admin.</p>
eventUpdateTotpSubject=Update TOTP
eventUpdateTotpBody=TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.
eventUpdateTotpBody=TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.
eventUpdateTotpBodyHtml=<p>TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin.</p>

View file

@ -1,12 +1,18 @@
emailVerificationSubject=Verifica\u00E7\u00E3o de e-mail
emailVerificationBody=Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email\n\n{0}\n\nEste link ir\u00E1 expirar dentro de {1} minutos.\n\nSe n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.
emailVerificationBodyHtml=<p>Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email</p><p><a href="{0}">{0}</a></p><p>Este link ir\u00E1 expirar dentro de {1} minutos.</p><p>Se n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.</p>
passwordResetSubject=Redefini\u00E7\u00E3o de senha
passwordResetBody=Algu\u00E9m pediu para mudar a senha de sua conta {2}. Se foi voc\u00EA, clique no link abaixo para definir uma nova senha\n\n{0}\n\nEste link ir\u00E1 expirar dentro de {1} minutos.\n\nSe voc\u00EA n\u00E3o deseja redefinir sua senha, basta ignorar esta mensagem e nada ser\u00E1 mudado.
passwordResetBodyHtml=<p>Algu\u00E9m pediu para mudar a senha de sua conta {2}. Se foi voc\u00EA, clique no link abaixo para definir uma nova senha</p><p><a href="{0}">{0}</a></p><p>Este link ir\u00E1 expirar dentro de {1} minutos.</p><p>Se voc\u00EA n\u00E3o deseja redefinir sua senha, basta ignorar esta mensagem e nada ser\u00E1 mudado.</p>
eventLoginErrorSubject=Erro de login
eventLoginErrorBody=Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.
eventLoginErrorBodyHtml=<p>Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.</p>
eventRemoveTotpSubject=Remover TOTP
eventRemoveTotpBody=TOTP foi removido da sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.
eventRemoveTotpBodyHtml=<p>TOTP foi removido da sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.</p>
eventUpdatePasswordSubject=Atualiza\u00E7\u00E3o de senha
eventUpdatePasswordBody=Sua senha foi alterada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.
eventUpdatePasswordBodyHtml=<p>Sua senha foi alterada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.</p>
eventUpdateTotpSubject=Atualiza\u00E7\u00E3o TOTP
eventUpdateTotpBody=TOTP foi atualizado para a sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.
eventUpdateTotpBodyHtml=<p>TOTP foi atualizado para a sua conta em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.</p>

View file

@ -1,11 +1,28 @@
package org.keycloak.email.freemarker;
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import org.jboss.logging.Logger;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.email.freemarker.beans.EventBean;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
@ -15,14 +32,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.text.MessageFormat;
import java.util.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -93,16 +102,29 @@ public class FreeMarkerEmailProvider implements EmailProvider {
Properties rb = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, rb));
String subject = new MessageFormat(rb.getProperty(subjectKey,subjectKey),locale).format(new Object[0]);
String body = freeMarker.processTemplate(attributes, template, theme);
String textTemplate = String.format("text/%s", template);
String textBody;
try {
textBody = freeMarker.processTemplate(attributes, textTemplate, theme);
} catch (final FreeMarkerException e ) {
textBody = null;
}
String htmlTemplate = String.format("html/%s", template);
String htmlBody;
try {
htmlBody = freeMarker.processTemplate(attributes, htmlTemplate, theme);
} catch (final FreeMarkerException e ) {
htmlBody = null;
}
send(subject, body);
send(subject, textBody, htmlBody);
} catch (Exception e) {
throw new EmailException("Failed to template email", e);
}
}
private void send(String subject, String body) throws EmailException {
private void send(String subject, String textBody, String htmlBody) throws EmailException {
try {
String address = user.getEmail();
Map<String, String> config = realm.getSmtpConfig();
@ -135,11 +157,25 @@ public class FreeMarkerEmailProvider implements EmailProvider {
Session session = Session.getInstance(props);
Multipart multipart = new MimeMultipart("alternative");
if(textBody != null) {
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "UTF-8");
multipart.addBodyPart(textPart);
}
if(htmlBody != null) {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlBody, "text/html; charset=UTF-8");
multipart.addBodyPart(htmlPart);
}
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
msg.setHeader("To", address);
msg.setSubject(subject);
msg.setText(body);
msg.setContent(multipart);
msg.saveChanges();
msg.setSentDate(new Date());

View file

@ -8,7 +8,7 @@ import java.util.regex.Pattern;
*/
public class MailUtil {
private static Pattern mailPattern = Pattern.compile("http[^\\s]*");
private static Pattern mailPattern = Pattern.compile("http[^\\s\"]*");
public static String getLink(String body) {
Matcher matcher = mailPattern.matcher(body);

View file

@ -49,7 +49,9 @@ import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
@ -119,9 +121,8 @@ public class RequiredActionEmailVerificationTest {
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String verificationUrl = MailUtil.getLink(body);
String verificationUrl = getPasswordResetEmailLink(message);
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
Event sendEvent = emailEvent.assertEvent();
@ -154,14 +155,12 @@ public class RequiredActionEmailVerificationTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
Event sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail("username", "verifyEmail").detail("email", "email@mail.com").assertEvent();
String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
String verificationUrl = MailUtil.getLink(body);
String verificationUrl = getPasswordResetEmailLink(message);
driver.navigate().to(verificationUrl.trim());
@ -192,11 +191,9 @@ public class RequiredActionEmailVerificationTest {
MimeMessage message = greenMail.getReceivedMessages()[1];
String body = (String) message.getContent();
events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
String verificationUrl = MailUtil.getLink(body);
String verificationUrl = getPasswordResetEmailLink(message);
driver.navigate().to(verificationUrl.trim());
@ -218,8 +215,7 @@ public class RequiredActionEmailVerificationTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String verificationUrl = MailUtil.getLink(body);
String verificationUrl = getPasswordResetEmailLink(message);
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
Event sendEvent = emailEvent.assertEvent();
@ -242,6 +238,27 @@ public class RequiredActionEmailVerificationTest {
assertTrue(loginPage.isCurrent());
}
private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
final String textContentType = multipart.getBodyPart(0).getContentType();
assertEquals("text/plain; charset=UTF-8", textContentType);
final String textBody = (String) multipart.getBodyPart(0).getContent();
final String textChangePwdUrl = MailUtil.getLink(textBody);
final String htmlContentType = multipart.getBodyPart(1).getContentType();
assertEquals("text/html; charset=UTF-8", htmlContentType);
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
final String htmlChangePwdUrl = MailUtil.getLink(htmlBody);
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
return htmlChangePwdUrl;
}
}

View file

@ -53,6 +53,7 @@ import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
@ -187,8 +188,7 @@ public abstract class AbstractIdentityProviderTest {
// read email to take verification link from
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String verificationUrl = MailUtil.getLink(body);
String verificationUrl = getVerificationEmailLink(message);
driver.navigate().to(verificationUrl.trim());
@ -805,4 +805,26 @@ public abstract class AbstractIdentityProviderTest {
}
}
}
private String getVerificationEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
final String textContentType = multipart.getBodyPart(0).getContentType();
assertEquals("text/plain; charset=UTF-8", textContentType);
final String textBody = (String) multipart.getBodyPart(0).getContent();
final String textVerificationUrl = MailUtil.getLink(textBody);
final String htmlContentType = multipart.getBodyPart(1).getContentType();
assertEquals("text/html; charset=UTF-8", htmlContentType);
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
final String htmlVerificationUrl = MailUtil.getLink(htmlBody);
assertEquals(htmlVerificationUrl, textVerificationUrl);
return htmlVerificationUrl;
}
}

View file

@ -52,7 +52,9 @@ import org.keycloak.util.Time;
import org.openqa.selenium.WebDriver;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Collections;
@ -146,9 +148,8 @@ public class ResetPasswordTest {
assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
final String changePasswordUrl = getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@ -211,8 +212,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
String changePasswordUrl = getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@ -256,8 +256,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
String changePasswordUrl = getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@ -294,8 +293,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
String changePasswordUrl = getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
@ -364,8 +362,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
String changePasswordUrl = getPasswordResetEmailLink(message);
Time.setOffset(350);
@ -513,8 +510,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
String changePasswordUrl = getPasswordResetEmailLink(message);
String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
@ -609,8 +605,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent();
String changePasswordUrl = MailUtil.getLink(body);
String changePasswordUrl = getPasswordResetEmailLink(message);
driver.manage().deleteAllCookies();
@ -629,5 +624,27 @@ public class ResetPasswordTest {
assertTrue(loginPage.isCurrent());
}
private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
final String textContentType = multipart.getBodyPart(0).getContentType();
assertEquals("text/plain; charset=UTF-8", textContentType);
final String textBody = (String) multipart.getBodyPart(0).getContent();
final String textChangePwdUrl = MailUtil.getLink(textBody);
final String htmlContentType = multipart.getBodyPart(1).getContentType();
assertEquals("text/html; charset=UTF-8", htmlContentType);
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
final String htmlChangePwdUrl = MailUtil.getLink(htmlBody);
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
return htmlChangePwdUrl;
}
}