keycloak-uncached
Changes
services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java 74(+46 -28)
Details
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 b62381b..c55a585 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<String, Object> attributes = new HashMap<String, Object>(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<String, Object> attributes = new HashMap<String, Object>(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<Object> subjectAttrs = Arrays.<Object>asList(idpAlias);
+ List<Object> subjectAttrs = Arrays.<Object> 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<String, Object> attributes = new HashMap<String, Object>(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<String, Object> attributes = new HashMap<String, Object>(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<String, Object> 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<String, Object> 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<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 (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 0000000..c62ac44
--- /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 0000000..6f52694
--- /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<Object> 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 fafc4ad..a3d4df1 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 b2142ef..bd371d9 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 @@
<html>
<body>
-${msg("emailVerificationBodyHtml",link, linkExpiration, realmName)?no_esc}
+${msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))?no_esc}
</body>
</html>
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 5999feb..6510dfc 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 @@
<html>
<body>
-${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText)?no_esc}
+${msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration))?no_esc}
</body>
</html>
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 31bddbe..fff38fc 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 @@
<html>
<body>
-${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration)?no_esc}
+${msg("identityProviderLinkBodyHtml", identityProviderAlias, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration))?no_esc}
</body>
</html>
\ 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 edbc888..e56ae1e 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 @@
<html>
<body>
-${msg("passwordResetBodyHtml",link, linkExpiration, realmName)?no_esc}
+${msg("passwordResetBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))?no_esc}
</body>
</html>
\ 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 5824d0a..e04e947 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=<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>
+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=<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 {3}.</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>
+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=<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 {5}.</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>
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=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">Link to reset credentials</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>
+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=<p>Someone just requested to change your {2} account''s credentials. If this was you, click on the link below to reset them.</p><p><a href="{0}">Link to reset credentials</a></p><p>This link will expire within {3}.</p><p>If you don''t want to reset your credentials, just ignore this message and nothing will be changed.</p>
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=<p>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.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {1} minutes.</p><p>If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.</p>
+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=<p>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.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {4}.</p><p>If you are unaware that your admin has requested this, just ignore this message and nothing will be changed.</p>
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=<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.</p>
@@ -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 152de22..9e39696 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 af9d5c4..6610c7a 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>, </#items></#list><#else></#if></#assign>
-${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 0232e12..ed9d246 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 e22649d..27405c9 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