From a5f675d6936ff657e7ad29e19389e035d64c5abb Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Fri, 26 Jan 2018 09:32:22 +0100 Subject: [PATCH] KEYCLOAK-4937 - convert time units in emails into human-friendly format --- .../FreeMarkerEmailTemplateProvider.java | 74 ++++++---- .../beans/LinkExpirationFormatterMethod.java | 75 ++++++++++ .../LinkExpirationFormatterMethodTest.java | 128 ++++++++++++++++++ .../keycloak/testsuite/admin/UserTest.java | 4 +- .../base/email/html/email-verification.ftl | 2 +- .../theme/base/email/html/executeActions.ftl | 2 +- .../email/html/identity-provider-link.ftl | 2 +- .../theme/base/email/html/password-reset.ftl | 2 +- .../email/messages/messages_en.properties | 30 ++-- .../base/email/text/email-verification.ftl | 2 +- .../theme/base/email/text/executeActions.ftl | 2 +- .../email/text/identity-provider-link.ftl | 2 +- .../theme/base/email/text/password-reset.ftl | 2 +- 13 files changed, 281 insertions(+), 46 deletions(-) create mode 100644 services/src/main/java/org/keycloak/theme/beans/LinkExpirationFormatterMethod.java create mode 100644 services/src/test/java/org/keycloak/theme/beans/LinkExpirationFormatterMethodTest.java 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 b62381b5f3..c55a5854fc 100755 --- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java +++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java @@ -17,6 +17,16 @@ package org.keycloak.email.freemarker; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.common.util.ObjectUtil; import org.keycloak.email.EmailException; @@ -34,25 +44,19 @@ import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.Theme; import org.keycloak.theme.ThemeProvider; +import org.keycloak.theme.beans.LinkExpirationFormatterMethod; import org.keycloak.theme.beans.MessageFormatterMethod; -import java.io.IOException; -import java.text.MessageFormat; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; - /** * @author Stian Thorgersen */ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { protected KeycloakSession session; - /** authenticationSession can be null for some email sendings, it is filled only for email sendings performed as part of the authentication session (email verification, password reset, broker link etc.)! */ + /** + * authenticationSession can be null for some email sendings, it is filled only for email sendings performed as part of the authentication session (email verification, password reset, broker link + * etc.)! + */ protected AuthenticationSessionModel authenticationSession; protected FreeMarkerUtil freeMarker; protected RealmModel realm; @@ -81,7 +85,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { attributes.put(name, value); return this; } - + @Override public EmailTemplateProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession) { this.authenticationSession = authenticationSession; @@ -109,8 +113,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException { Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); - attributes.put("link", link); - attributes.put("linkExpiration", expirationInMinutes); + addLinkInfoIntoAttributes(link, expirationInMinutes, attributes); attributes.put("realmName", getRealmName()); @@ -134,8 +137,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { public void sendConfirmIdentityBrokerLink(String link, long expirationInMinutes) throws EmailException { Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); - attributes.put("link", link); - attributes.put("linkExpiration", expirationInMinutes); + addLinkInfoIntoAttributes(link, expirationInMinutes, attributes); attributes.put("realmName", getRealmName()); @@ -146,7 +148,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { attributes.put("identityProviderContext", brokerContext); attributes.put("identityProviderAlias", idpAlias); - List subjectAttrs = Arrays.asList(idpAlias); + List subjectAttrs = Arrays. asList(idpAlias); send("identityProviderLinkSubject", subjectAttrs, "identity-provider-link.ftl", attributes); } @@ -154,8 +156,7 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException { Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); - attributes.put("link", link); - attributes.put("linkExpiration", expirationInMinutes); + addLinkInfoIntoAttributes(link, expirationInMinutes, attributes); attributes.put("realmName", getRealmName()); @@ -166,14 +167,31 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException { Map attributes = new HashMap(this.attributes); attributes.put("user", new ProfileBean(user)); - attributes.put("link", link); - attributes.put("linkExpiration", expirationInMinutes); + addLinkInfoIntoAttributes(link, expirationInMinutes, attributes); attributes.put("realmName", getRealmName()); send("emailVerificationSubject", "email-verification.ftl", attributes); } + /** + * Add link info into template attributes. + * + * @param link to add + * @param expirationInMinutes to add + * @param attributes to add link info into + */ + protected void addLinkInfoIntoAttributes(String link, long expirationInMinutes, Map attributes) throws EmailException { + attributes.put("link", link); + attributes.put("linkExpiration", expirationInMinutes); + try { + Locale locale = session.getContext().resolveLocale(user); + attributes.put("linkExpirationFormatter", new LinkExpirationFormatterMethod(getTheme().getMessages(locale), locale)); + } catch (IOException e) { + throw new EmailException("Failed to template email", e); + } + } + protected void send(String subjectKey, String template, Map attributes) throws EmailException { send(subjectKey, Collections.emptyList(), template, attributes); } @@ -185,19 +203,19 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { attributes.put("locale", locale); Properties rb = theme.getMessages(locale); attributes.put("msg", new MessageFormatterMethod(locale, rb)); - String subject = new MessageFormat(rb.getProperty(subjectKey,subjectKey),locale).format(subjectAttributes.toArray()); + String subject = new MessageFormat(rb.getProperty(subjectKey, subjectKey), locale).format(subjectAttributes.toArray()); String textTemplate = String.format("text/%s", template); String textBody; try { textBody = freeMarker.processTemplate(attributes, textTemplate, theme); - } catch (final FreeMarkerException e ) { + } 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 ) { + } catch (final FreeMarkerException e) { htmlBody = null; } @@ -210,12 +228,12 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { protected Theme getTheme() throws IOException { return session.theme().getTheme(Theme.Type.EMAIL); } - + protected 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 (EmailException e){ + } catch (EmailException e) { throw e; } catch (Exception e) { throw new EmailException("Failed to template email", e); @@ -235,9 +253,9 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider { public void close() { } - protected String toCamelCase(EventType event){ + protected String toCamelCase(EventType event) { StringBuilder sb = new StringBuilder("event"); - for(String s : event.name().toLowerCase().split("_")){ + for (String s : event.name().toLowerCase().split("_")) { sb.append(ObjectUtil.capitalize(s)); } return sb.toString(); diff --git a/services/src/main/java/org/keycloak/theme/beans/LinkExpirationFormatterMethod.java b/services/src/main/java/org/keycloak/theme/beans/LinkExpirationFormatterMethod.java new file mode 100644 index 0000000000..c62ac447c8 --- /dev/null +++ b/services/src/main/java/org/keycloak/theme/beans/LinkExpirationFormatterMethod.java @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2018 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.theme.beans; + +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; + +/** + * Method used to format link expiration time period in emails. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class LinkExpirationFormatterMethod implements TemplateMethodModelEx { + + protected final Properties messages; + protected final Locale locale; + + public LinkExpirationFormatterMethod(Properties messages, Locale locale) { + this.messages = messages; + this.locale = locale; + } + + @SuppressWarnings("rawtypes") + @Override + public Object exec(List arguments) throws TemplateModelException { + Object val = arguments.isEmpty() ? null : arguments.get(0); + if (val == null) + return ""; + + try { + //input value is in minutes, as defined in EmailTemplateProvider! + return format(Long.parseLong(val.toString().trim()) * 60); + } catch (NumberFormatException e) { + // not a number, return it as is + return val.toString(); + } + + } + + protected String format(long valueInSeconds) { + + String unitKey = "seconds"; + long value = valueInSeconds; + + if (value > 0 && value % 60 == 0) { + unitKey = "minutes"; + value = value / 60; + if (value % 60 == 0) { + unitKey = "hours"; + value = value / 60; + if (value % 24 == 0) { + unitKey = "days"; + value = value / 24; + } + } + } + + return value + " " + getUnitTextFromMessages(unitKey, value); + } + + protected String getUnitTextFromMessages(String unitKey, long value) { + String msg = messages.getProperty("linkExpirationFormatter.timePeriodUnit." + unitKey + "." + value); + if (msg != null) + return msg; + return messages.getProperty("linkExpirationFormatter.timePeriodUnit." + unitKey); + } + +} diff --git a/services/src/test/java/org/keycloak/theme/beans/LinkExpirationFormatterMethodTest.java b/services/src/test/java/org/keycloak/theme/beans/LinkExpirationFormatterMethodTest.java new file mode 100644 index 0000000000..6f52694a41 --- /dev/null +++ b/services/src/test/java/org/keycloak/theme/beans/LinkExpirationFormatterMethodTest.java @@ -0,0 +1,128 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2018 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.theme.beans; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; + +import freemarker.template.TemplateModelException; + +/** + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class LinkExpirationFormatterMethodTest { + + protected static final Locale locale = Locale.ENGLISH; + protected static final Properties messages = new Properties(); + static { + messages.put("linkExpirationFormatter.timePeriodUnit.seconds.1", "second"); + messages.put("linkExpirationFormatter.timePeriodUnit.seconds", "seconds"); + messages.put("linkExpirationFormatter.timePeriodUnit.minutes.1", "minute"); + messages.put("linkExpirationFormatter.timePeriodUnit.minutes.3", "minutes-3"); + messages.put("linkExpirationFormatter.timePeriodUnit.minutes", "minutes"); + messages.put("linkExpirationFormatter.timePeriodUnit.hours.1", "hour"); + messages.put("linkExpirationFormatter.timePeriodUnit.hours", "hours"); + messages.put("linkExpirationFormatter.timePeriodUnit.days.1", "day"); + messages.put("linkExpirationFormatter.timePeriodUnit.days", "days"); + } + + protected List toList(Object... objects) { + return Arrays.asList(objects); + } + + @Test + public void inputtypes_null() throws TemplateModelException{ + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("", tested.exec(Collections.emptyList())); + } + + @Test + public void inputtypes_string_empty() throws TemplateModelException{ + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("", tested.exec(toList(""))); + Assert.assertEquals(" ", tested.exec(toList(" "))); + } + + @Test + public void inputtypes_string_number() throws TemplateModelException{ + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("2 minutes", tested.exec(toList("2"))); + Assert.assertEquals("2 minutes", tested.exec(toList(" 2 "))); + } + + @Test + public void inputtypes_string_notanumber() throws TemplateModelException{ + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("ahoj", tested.exec(toList("ahoj"))); + } + + @Test + public void inputtypes_number() throws TemplateModelException{ + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("5 minutes", tested.exec(toList(new Integer(5)))); + Assert.assertEquals("5 minutes", tested.exec(toList(new Long(5)))); + } + + @Test + public void format_second_zero() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("0 seconds", tested.exec(toList(0))); + } + + @Test + public void format_minute_one() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("1 minute", tested.exec(toList(1))); + } + + @Test + public void format_minute_more() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("2 minutes", tested.exec(toList(2))); + //test support for languages with more plurals depending on the value + Assert.assertEquals("3 minutes-3", tested.exec(toList(3))); + Assert.assertEquals("5 minutes", tested.exec(toList(5))); + Assert.assertEquals("24 minutes", tested.exec(toList(24))); + Assert.assertEquals("59 minutes", tested.exec(toList(59))); + Assert.assertEquals("61 minutes", tested.exec(toList(61))); + } + + @Test + public void format_hour_one() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("1 hour", tested.exec(toList(60))); + } + + @Test + public void format_hour_more() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("2 hours", tested.exec(toList(2 * 60))); + Assert.assertEquals("5 hours", tested.exec(toList(5 * 60))); + Assert.assertEquals("23 hours", tested.exec(toList(23 * 60))); + Assert.assertEquals("25 hours", tested.exec(toList(25 * 60))); + } + + @Test + public void format_day_one() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("1 day", tested.exec(toList(60 * 24))); + } + + @Test + public void format_day_more() throws TemplateModelException { + LinkExpirationFormatterMethod tested = new LinkExpirationFormatterMethod(messages, locale); + Assert.assertEquals("2 days", tested.exec(toList(2 * 24 * 60))); + Assert.assertEquals("5 days", tested.exec(toList(5 * 24 * 60))); + } + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index fafc4add71..a3d4df16aa 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -685,11 +685,11 @@ public class UserTest extends AbstractAdminTest { assertTrue(body.getText().contains("Update Password")); assertTrue(body.getText().contains("your Admin-client-test account")); - assertTrue(body.getText().contains("This link will expire within 720 minutes")); + assertTrue(body.getText().contains("This link will expire within 12 hours")); assertTrue(body.getHtml().contains("Update Password")); assertTrue(body.getHtml().contains("your Admin-client-test account")); - assertTrue(body.getHtml().contains("This link will expire within 720 minutes")); + assertTrue(body.getHtml().contains("This link will expire within 12 hours")); String link = MailUtils.getPasswordResetEmailLink(body); diff --git a/themes/src/main/resources/theme/base/email/html/email-verification.ftl b/themes/src/main/resources/theme/base/email/html/email-verification.ftl index b2142ef9e4..bd371d9db1 100644 --- a/themes/src/main/resources/theme/base/email/html/email-verification.ftl +++ b/themes/src/main/resources/theme/base/email/html/email-verification.ftl @@ -1,5 +1,5 @@ -${msg("emailVerificationBodyHtml",link, linkExpiration, realmName)?no_esc} +${msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))?no_esc} diff --git a/themes/src/main/resources/theme/base/email/html/executeActions.ftl b/themes/src/main/resources/theme/base/email/html/executeActions.ftl index 5999feb043..6510dfc116 100755 --- a/themes/src/main/resources/theme/base/email/html/executeActions.ftl +++ b/themes/src/main/resources/theme/base/email/html/executeActions.ftl @@ -4,6 +4,6 @@ -${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText)?no_esc} +${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration))?no_esc} diff --git a/themes/src/main/resources/theme/base/email/html/identity-provider-link.ftl b/themes/src/main/resources/theme/base/email/html/identity-provider-link.ftl index 31bddbef30..fff38fc330 100644 --- a/themes/src/main/resources/theme/base/email/html/identity-provider-link.ftl +++ b/themes/src/main/resources/theme/base/email/html/identity-provider-link.ftl @@ -1,5 +1,5 @@ -${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)?no_esc} +${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration))?no_esc} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/html/password-reset.ftl b/themes/src/main/resources/theme/base/email/html/password-reset.ftl index edbc888aac..e56ae1ea89 100755 --- a/themes/src/main/resources/theme/base/email/html/password-reset.ftl +++ b/themes/src/main/resources/theme/base/email/html/password-reset.ftl @@ -1,5 +1,5 @@ -${msg("passwordResetBodyHtml",link, linkExpiration, realmName)?no_esc} +${msg("passwordResetBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))?no_esc} \ No newline at end of file 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 5824d0a12d..e04e947a44 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,18 +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=

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.

+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 {3}.\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 {3}.

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

+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 {5}.\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 {5}.

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

passwordResetSubject=Reset password -passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed. -passwordResetBodyHtml=

Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.

Link to reset credentials

This link will expire within {1} minutes.

If you don''t want to reset your credentials, just ignore this message and nothing will be changed.

+passwordResetBody=Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {3}.\n\nIf you don''t want to reset your credentials, just ignore this message and nothing will be changed. +passwordResetBodyHtml=

Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.

Link to reset credentials

This link will expire within {3}.

If you don''t want to reset your credentials, just ignore this message and nothing will be changed.

executeActionsSubject=Update Your Account -executeActionsBody=Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed. -executeActionsBodyHtml=

Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.

Link to account update

This link will expire within {1} minutes.

If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.

+executeActionsBody=Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you are unaware that your admin has requested this, just ignore this message and nothing will be changed. +executeActionsBodyHtml=

Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.

Link to account update

This link will expire within {4}.

If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.

eventLoginErrorSubject=Login error eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin. eventLoginErrorBodyHtml=

A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.

@@ -31,3 +31,17 @@ requiredAction.terms_and_conditions=Terms and Conditions requiredAction.UPDATE_PASSWORD=Update Password requiredAction.UPDATE_PROFILE=Update Profile requiredAction.VERIFY_EMAIL=Verify Email + +# units for link expiration timeout formatting +linkExpirationFormatter.timePeriodUnit.seconds=seconds +linkExpirationFormatter.timePeriodUnit.seconds.1=second +linkExpirationFormatter.timePeriodUnit.minutes=minutes +linkExpirationFormatter.timePeriodUnit.minutes.1=minute +#for language which have more unit plural forms depending on the value (eg. Czech and other Slavic langs) you can override unit text for some other values like this: +#linkExpirationFormatter.timePeriodUnit.minutes.2=minuty +#linkExpirationFormatter.timePeriodUnit.minutes.3=minuty +#linkExpirationFormatter.timePeriodUnit.minutes.4=minuty +linkExpirationFormatter.timePeriodUnit.hours=hours +linkExpirationFormatter.timePeriodUnit.hours.1=hour +linkExpirationFormatter.timePeriodUnit.days=days +linkExpirationFormatter.timePeriodUnit.days.1=day diff --git a/themes/src/main/resources/theme/base/email/text/email-verification.ftl b/themes/src/main/resources/theme/base/email/text/email-verification.ftl index 152de221b8..9e39696320 100644 --- a/themes/src/main/resources/theme/base/email/text/email-verification.ftl +++ b/themes/src/main/resources/theme/base/email/text/email-verification.ftl @@ -1,2 +1,2 @@ <#ftl output_format="plainText"> -${msg("emailVerificationBody",link, linkExpiration, realmName)} \ No newline at end of file +${msg("emailVerificationBody",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/text/executeActions.ftl b/themes/src/main/resources/theme/base/email/text/executeActions.ftl index af9d5c4d6a..6610c7adab 100755 --- a/themes/src/main/resources/theme/base/email/text/executeActions.ftl +++ b/themes/src/main/resources/theme/base/email/text/executeActions.ftl @@ -1,4 +1,4 @@ <#ftl output_format="plainText"> <#assign requiredActionsText><#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, <#else> -${msg("executeActionsBody",link, linkExpiration, realmName, requiredActionsText)} \ No newline at end of file +${msg("executeActionsBody",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration))} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/text/identity-provider-link.ftl b/themes/src/main/resources/theme/base/email/text/identity-provider-link.ftl index 0232e12969..ed9d246cff 100644 --- a/themes/src/main/resources/theme/base/email/text/identity-provider-link.ftl +++ b/themes/src/main/resources/theme/base/email/text/identity-provider-link.ftl @@ -1,2 +1,2 @@ <#ftl output_format="plainText"> -${msg("identityProviderLinkBody", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)} \ No newline at end of file +${msg("identityProviderLinkBody", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration))} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/email/text/password-reset.ftl b/themes/src/main/resources/theme/base/email/text/password-reset.ftl index e22649d56a..27405c9a42 100755 --- a/themes/src/main/resources/theme/base/email/text/password-reset.ftl +++ b/themes/src/main/resources/theme/base/email/text/password-reset.ftl @@ -1,2 +1,2 @@ <#ftl output_format="plainText"> -${msg("passwordResetBody",link, linkExpiration, realmName)} \ No newline at end of file +${msg("passwordResetBody",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))} \ No newline at end of file