keycloak-uncached

Changes

forms/account-api/src/main/java/org/keycloak/account/Account.java 38(+0 -38)

forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java 201(+0 -201)

forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider 1(+0 -1)

forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider 2(+0 -2)

forms/login-api/src/main/java/org/keycloak/login/LoginForms.java 52(+0 -52)

forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java 287(+0 -287)

forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider 1(+0 -1)

forms/pom.xml 2(+2 -0)

pom.xml 5(+5 -0)

server/pom.xml 10(+10 -0)

services/pom.xml 6(+6 -0)

services/src/main/java/org/keycloak/services/email/EmailSender.java 162(+0 -162)

services/src/test/java/org/keycloak/services/email/EmailSenderTest.java 87(+0 -87)

Details

diff --git a/docbook/reference/en/en-US/modules/themes.xml b/docbook/reference/en/en-US/modules/themes.xml
index 5bb39cb..5e4ff8a 100755
--- a/docbook/reference/en/en-US/modules/themes.xml
+++ b/docbook/reference/en/en-US/modules/themes.xml
@@ -148,26 +148,26 @@
             <title>Account SPI</title>
             <para>
                 The Account SPI allows implementing the account management pages using whatever web framework or templating
-                engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProvider</literal>
-                and <literal>org.keycloak.account.Account</literal> in <literal>forms/account-api</literal>.
+                engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProviderFactory</literal>
+                and <literal>org.keycloak.account.AccountProvider</literal> in <literal>forms/account-api</literal>.
             </para>
             <para>
                 Keycloaks default account management provider is built on the FreeMarker template engine (<literal>forms/account-freemarker</literal>).
                 To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-account-freemarker-1.0-beta-1-SNAPSHOT.jar</literal>
-                or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProvider</literal>.
+                or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory</literal>.
             </para>
         </section>
         <section>
             <title>Login SPI</title>
             <para>
                 The Login SPI allows implementing the login forms using whatever web framework or templating
-                engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProvider</literal>
-                and <literal>org.keycloak.login.LoginForms</literal> in <literal>forms/login-api</literal>.
+                engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProviderFactory</literal>
+                and <literal>org.keycloak.login.LoginFormsProvider</literal> in <literal>forms/login-api</literal>.
             </para>
             <para>
                 Keycloaks default login forms provider is built on the FreeMarker template engine (<literal>forms/login-freemarker</literal>).
                 To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-login-freemarker-1.0-beta-1-SNAPSHOT.jar</literal>
-                or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider</literal>.
+                or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory</literal>.
             </para>
         </section>
     </section>
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java
index 37ffe89..f021e84 100644
--- a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java
@@ -1,12 +1,41 @@
 package org.keycloak.account;
 
+import org.keycloak.audit.Event;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.provider.Provider;
+
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+import java.util.List;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public interface AccountProvider {
+public interface AccountProvider extends Provider {
+
+    AccountProvider setUriInfo(UriInfo uriInfo);
+
+    Response createResponse(AccountPages page);
+
+    AccountProvider setError(String message);
+
+    AccountProvider setSuccess(String message);
+
+    AccountProvider setWarning(String message);
+
+    AccountProvider setUser(UserModel user);
+
+    AccountProvider setStatus(Response.Status status);
+
+    AccountProvider setRealm(RealmModel realm);
+
+    AccountProvider setReferrer(String[] referrer);
+
+    AccountProvider setEvents(List<Event> events);
 
-    public Account createAccount(UriInfo uriInfo);
+    AccountProvider setSessions(List<UserSessionModel> sessions);
 
+    AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
 }
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java b/forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java
new file mode 100644
index 0000000..e956b14
--- /dev/null
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java
@@ -0,0 +1,27 @@
+package org.keycloak.account;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountSpi implements Spi {
+
+    @Override
+    public String getName() {
+        return "account";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return AccountProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return AccountProviderFactory.class;
+    }
+
+}
diff --git a/forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..8ab9e17
--- /dev/null
+++ b/forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1 @@
+org.keycloak.account.AccountSpi
\ No newline at end of file
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
old mode 100644
new mode 100755
index 17d8fb9..0400f75
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -1,18 +1,214 @@
 package org.keycloak.account.freemarker;
 
-import org.keycloak.account.Account;
+import org.jboss.logging.Logger;
+import org.keycloak.account.AccountPages;
 import org.keycloak.account.AccountProvider;
+import org.keycloak.account.freemarker.model.AccountBean;
+import org.keycloak.account.freemarker.model.AccountSocialBean;
+import org.keycloak.account.freemarker.model.FeaturesBean;
+import org.keycloak.account.freemarker.model.LogBean;
+import org.keycloak.account.freemarker.model.MessageBean;
+import org.keycloak.account.freemarker.model.ReferrerBean;
+import org.keycloak.account.freemarker.model.SessionsBean;
+import org.keycloak.account.freemarker.model.TotpBean;
+import org.keycloak.account.freemarker.model.UrlBean;
+import org.keycloak.audit.Event;
+import org.keycloak.freemarker.ExtendingThemeManager;
+import org.keycloak.freemarker.FreeMarkerException;
+import org.keycloak.freemarker.FreeMarkerUtil;
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.provider.ProviderSession;
 
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class FreeMarkerAccountProvider implements AccountProvider {
 
+    private static final Logger logger = Logger.getLogger(FreeMarkerAccountProvider.class);
+
+    private UserModel user;
+    private Response.Status status = Response.Status.OK;
+    private RealmModel realm;
+    private String[] referrer;
+    private List<Event> events;
+    private List<UserSessionModel> sessions;
+    private boolean social;
+    private boolean audit;
+    private boolean passwordUpdateSupported;
+    private ProviderSession session;
+
+    public static enum MessageType {SUCCESS, WARNING, ERROR}
+
+    private UriInfo uriInfo;
+
+    private String message;
+    private MessageType messageType;
+
+    public FreeMarkerAccountProvider(ProviderSession session) {
+        this.session = session;
+    }
+
+    public AccountProvider setUriInfo(UriInfo uriInfo) {
+        this.uriInfo = uriInfo;
+        return this;
+    }
+
+    @Override
+    public Response createResponse(AccountPages page) {
+        Map<String, Object> attributes = new HashMap<String, Object>();
+
+        ExtendingThemeManager themeManager = new ExtendingThemeManager(session);
+        Theme theme;
+        try {
+            theme = themeManager.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT);
+        } catch (IOException e) {
+            logger.error("Failed to create theme", e);
+            return Response.serverError().build();
+        }
+
+        try {
+            attributes.put("properties", theme.getProperties());
+        } catch (IOException e) {
+            logger.warn("Failed to load properties", e);
+        }
+
+        Properties messages;
+        try {
+            messages = theme.getMessages();
+            attributes.put("rb", messages);
+        } catch (IOException e) {
+            logger.warn("Failed to load messages", e);
+            messages = new Properties();
+        }
+
+        URI baseUri = uriInfo.getBaseUri();
+        UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
+        for (Map.Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
+           baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray());
+        }
+        URI baseQueryUri = baseUriBuilder.build();
+
+        if (message != null) {
+            attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
+        }
+
+        if (referrer != null) {
+            attributes.put("referrer", new ReferrerBean(referrer));
+        }
+
+        attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri()));
+
+        attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported));
+
+        switch (page) {
+            case ACCOUNT:
+                attributes.put("account", new AccountBean(user));
+                break;
+            case TOTP:
+                attributes.put("totp", new TotpBean(user, baseUri));
+                break;
+            case SOCIAL:
+                attributes.put("social", new AccountSocialBean(realm, user, uriInfo.getBaseUri()));
+                break;
+            case LOG:
+                attributes.put("log", new LogBean(events));
+                break;
+            case SESSIONS:
+                attributes.put("sessions", new SessionsBean(realm, sessions));
+                break;
+        }
+
+        try {
+            String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
+            return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
+        } catch (FreeMarkerException e) {
+            logger.error("Failed to process template", e);
+            return Response.serverError().build();
+        }
+    }
+
+    @Override
+    public AccountProvider setError(String message) {
+        this.message = message;
+        this.messageType = MessageType.ERROR;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setSuccess(String message) {
+        this.message = message;
+        this.messageType = MessageType.SUCCESS;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setWarning(String message) {
+        this.message = message;
+        this.messageType = MessageType.WARNING;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setUser(UserModel user) {
+        this.user = user;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setRealm(RealmModel realm) {
+        this.realm = realm;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setStatus(Response.Status status) {
+        this.status = status;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setReferrer(String[] referrer) {
+        this.referrer = referrer;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setEvents(List<Event> events) {
+        this.events = events;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setSessions(List<UserSessionModel> sessions) {
+        this.sessions = sessions;
+        return this;
+    }
+
+    @Override
+    public AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
+        this.social = social;
+        this.audit = audit;
+        this.passwordUpdateSupported = passwordUpdateSupported;
+        return this;
+    }
+
     @Override
-    public Account createAccount(UriInfo uriInfo) {
-        return new FreeMarkerAccount(uriInfo);
+    public void close() {
     }
 
 }
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java
new file mode 100644
index 0000000..7a3f272
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java
@@ -0,0 +1,33 @@
+package org.keycloak.account.freemarker;
+
+import org.keycloak.Config;
+import org.keycloak.account.AccountProvider;
+import org.keycloak.account.AccountProviderFactory;
+import org.keycloak.provider.ProviderSession;
+
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerAccountProviderFactory implements AccountProviderFactory {
+
+    @Override
+    public AccountProvider create(ProviderSession providerSession) {
+        return new FreeMarkerAccountProvider(providerSession);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public String getId() {
+        return "freemarker";
+    }
+
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java
index 08f156a..6fc48be 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java
@@ -21,7 +21,7 @@
  */
 package org.keycloak.account.freemarker.model;
 
-import org.keycloak.account.freemarker.FreeMarkerAccount;
+import org.keycloak.account.freemarker.FreeMarkerAccountProvider;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -30,9 +30,9 @@ public class MessageBean {
 
     private String summary;
 
-    private FreeMarkerAccount.MessageType type;
+    private FreeMarkerAccountProvider.MessageType type;
 
-    public MessageBean(String message, FreeMarkerAccount.MessageType type) {
+    public MessageBean(String message, FreeMarkerAccountProvider.MessageType type) {
         this.summary = message;
         this.type = type;
     }
@@ -46,15 +46,15 @@ public class MessageBean {
     }
 
     public boolean isSuccess() {
-        return FreeMarkerAccount.MessageType.SUCCESS.equals(this.type);
+        return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type);
     }
 
     public boolean isWarning() {
-        return FreeMarkerAccount.MessageType.WARNING.equals(this.type);
+        return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type);
     }
 
     public boolean isError() {
-        return FreeMarkerAccount.MessageType.ERROR.equals(this.type);
+        return FreeMarkerAccountProvider.MessageType.ERROR.equals(this.type);
     }
 
 }
\ No newline at end of file
diff --git a/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory
new file mode 100644
index 0000000..fd99df6
--- /dev/null
+++ b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory
@@ -0,0 +1 @@
+org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory
\ No newline at end of file
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
index dd02e62..14794fb 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
@@ -10,7 +10,7 @@ import java.util.Properties;
  */
 public interface Theme {
 
-    public enum Type { LOGIN, ACCOUNT, ADMIN, COMMON };
+    public enum Type { LOGIN, ACCOUNT, ADMIN, EMAIL, COMMON };
 
     public String getName();
 
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java
index 6ca677c..07ee09a 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java
@@ -1,12 +1,14 @@
 package org.keycloak.freemarker;
 
+import org.keycloak.provider.Provider;
+
 import java.io.IOException;
 import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public interface ThemeProvider {
+public interface ThemeProvider extends Provider {
 
     public int getProviderPriority();
 
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java
new file mode 100644
index 0000000..26ce238
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java
@@ -0,0 +1,9 @@
+package org.keycloak.freemarker;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ThemeProviderFactory extends ProviderFactory<ThemeProvider> {
+}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java
new file mode 100644
index 0000000..c3d738b
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java
@@ -0,0 +1,25 @@
+package org.keycloak.freemarker;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ThemeSpi implements Spi {
+    @Override
+    public String getName() {
+        return "theme";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ThemeProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ThemeProviderFactory.class;
+    }
+}
diff --git a/forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..4ba538b
--- /dev/null
+++ b/forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1 @@
+org.keycloak.freemarker.ThemeSpi
\ No newline at end of file
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java
index 8a8659c..47f1fdd 100644
--- a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java
@@ -20,12 +20,14 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
     private static Set<String> ACCOUNT_THEMES = new HashSet<String>();
     private static Set<String> LOGIN_THEMES = new HashSet<String>();
     private static Set<String> ADMIN_THEMES = new HashSet<String>();
+    private static Set<String> EMAIL_THEMES = new HashSet<String>();
     private static Set<String> COMMON_THEMES = new HashSet<String>();
 
     static {
         Collections.addAll(ACCOUNT_THEMES, BASE, PATTERNFLY, KEYCLOAK);
         Collections.addAll(LOGIN_THEMES, BASE, PATTERNFLY, KEYCLOAK);
         Collections.addAll(ADMIN_THEMES, BASE, PATTERNFLY, KEYCLOAK);
+        Collections.addAll(EMAIL_THEMES, KEYCLOAK);
         Collections.addAll(COMMON_THEMES, KEYCLOAK);
     }
 
@@ -52,6 +54,8 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
                 return ACCOUNT_THEMES;
             case ADMIN:
                 return ADMIN_THEMES;
+            case EMAIL:
+                return EMAIL_THEMES;
             case COMMON:
                 return COMMON_THEMES;
             default:
@@ -64,4 +68,8 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
         return nameSet(type).contains(name);
     }
 
+    @Override
+    public void close() {
+    }
+
 }
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java
new file mode 100644
index 0000000..4c3515d
--- /dev/null
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java
@@ -0,0 +1,35 @@
+package org.keycloak.theme;
+
+import org.keycloak.Config;
+import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.freemarker.ThemeProviderFactory;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DefaultKeycloakThemeProviderFactory implements ThemeProviderFactory {
+
+    private DefaultKeycloakThemeProvider themeProvider;
+
+    @Override
+    public ThemeProvider create(ProviderSession providerSession) {
+        return themeProvider;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+        themeProvider = new DefaultKeycloakThemeProvider();
+    }
+
+    @Override
+    public void close() {
+        themeProvider = null;
+    }
+
+    @Override
+    public String getId() {
+        return "default";
+    }
+
+}
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java
index 101851c..96cac2f 100644
--- a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java
@@ -18,11 +18,8 @@ public class FolderThemeProvider implements ThemeProvider {
 
     private File rootDir;
 
-    public FolderThemeProvider() {
-        String d = Config.scope("theme").get("dir");
-        if (d != null) {
-            rootDir = new File(d);
-        }
+    public FolderThemeProvider(File rootDir) {
+        this.rootDir = rootDir;
     }
 
     @Override
@@ -75,4 +72,8 @@ public class FolderThemeProvider implements ThemeProvider {
         return typeDir != null && new File(typeDir, name).isDirectory();
     }
 
+    @Override
+    public void close() {
+    }
+
 }
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java
new file mode 100644
index 0000000..ff9dd2e
--- /dev/null
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java
@@ -0,0 +1,41 @@
+package org.keycloak.theme;
+
+import org.keycloak.Config;
+import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.freemarker.ThemeProviderFactory;
+import org.keycloak.provider.ProviderSession;
+
+import java.io.File;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FolderThemeProviderFactory implements ThemeProviderFactory {
+
+    private FolderThemeProvider themeProvider;
+
+    @Override
+    public ThemeProvider create(ProviderSession providerSession) {
+        return themeProvider;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+        String d = config.get("dir");
+        File rootDir = null;
+        if (d != null) {
+            rootDir = new File(d);
+        }
+        themeProvider = new FolderThemeProvider(rootDir);
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return "folder";
+    }
+}
diff --git a/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory
new file mode 100644
index 0000000..c1b32dc
--- /dev/null
+++ b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory
@@ -0,0 +1,2 @@
+org.keycloak.theme.DefaultKeycloakThemeProviderFactory
+org.keycloak.theme.FolderThemeProviderFactory
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl
new file mode 100644
index 0000000..38d150f
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl
@@ -0,0 +1,5 @@
+Someone has created a Keycloak account with this email address. If this was you, click the link below to verify your email address:
+${link}
+This link will expire within ${linkExpiration} minutes.
+
+If you didn't create this account, just ignore this message.
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties
new file mode 100755
index 0000000..3139aca
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties
@@ -0,0 +1,2 @@
+emailVerificationSubject=Verify email
+passwordResetSubject=Reset password
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl
new file mode 100644
index 0000000..5d277e5
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl
@@ -0,0 +1,5 @@
+Someone just requested to change your Keycloak account's password. If this was you, click on the link below to set a new password:
+${link}
+This link will expire within ${linkExpiration} minutes.
+
+If you don't want to reset your password, just ignore this message and nothing will be changed.
\ No newline at end of file
diff --git a/forms/email-api/pom.xml b/forms/email-api/pom.xml
new file mode 100755
index 0000000..19ef08a
--- /dev/null
+++ b/forms/email-api/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-beta-1-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-email-api</artifactId>
+	<name>Keycloak Email API</name>
+	<description />
+
+	<dependencies>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-core</artifactId>
+			<version>${project.version}</version>
+            <scope>provided</scope>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+                                    <source>${maven.compiler.source}</source>
+                                    <target>${maven.compiler.target}</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
new file mode 100644
index 0000000..0827b96
--- /dev/null
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
@@ -0,0 +1,20 @@
+package org.keycloak.email;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface EmailProvider extends Provider {
+
+    public EmailProvider setRealm(RealmModel realm);
+
+    public EmailProvider setUser(UserModel user);
+
+    public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
+
+    public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
+
+}
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java
new file mode 100644
index 0000000..02d7daf
--- /dev/null
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java
@@ -0,0 +1,9 @@
+package org.keycloak.email;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface EmailProviderFactory extends ProviderFactory<EmailProvider> {
+}
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java b/forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java
new file mode 100644
index 0000000..cb2877b
--- /dev/null
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java
@@ -0,0 +1,25 @@
+package org.keycloak.email;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class EmailSpi implements Spi {
+    @Override
+    public String getName() {
+        return "email";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return EmailProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return EmailProviderFactory.class;
+    }
+}
diff --git a/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..4110dba
--- /dev/null
+++ b/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1 @@
+org.keycloak.email.EmailSpi
\ No newline at end of file
diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml
new file mode 100755
index 0000000..b5fb4de
--- /dev/null
+++ b/forms/email-freemarker/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-forms</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.0-beta-1-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>keycloak-email-freemarker</artifactId>
+    <name>Keycloak Email FreeMarker</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>${maven.compiler.source}</source>
+                    <target>${maven.compiler.target}</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
new file mode 100644
index 0000000..9590a9c
--- /dev/null
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
@@ -0,0 +1,140 @@
+package org.keycloak.email.freemarker;
+
+import org.jboss.logging.Logger;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.freemarker.ExtendingThemeManager;
+import org.keycloak.freemarker.FreeMarkerUtil;
+import org.keycloak.freemarker.Theme;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderSession;
+
+import javax.mail.Message;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerEmailProvider implements EmailProvider {
+
+    private static final Logger log = Logger.getLogger(FreeMarkerEmailProvider.class);
+
+    private ProviderSession session;
+    private RealmModel realm;
+    private UserModel user;
+
+    public FreeMarkerEmailProvider(ProviderSession session) {
+        this.session = session;
+    }
+
+    @Override
+    public EmailProvider setRealm(RealmModel realm) {
+        this.realm = realm;
+        return this;
+    }
+
+    @Override
+    public EmailProvider setUser(UserModel user) {
+        this.user = user;
+        return this;
+    }
+
+    @Override
+    public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
+        Map<String, Object> attributes = new HashMap<String, Object>();
+        attributes.put("link", link);
+        attributes.put("linkExpiration", expirationInMinutes);
+
+        send("passwordResetSubject", "password-reset.ftl", attributes);
+    }
+
+    @Override
+    public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException {
+        Map<String, Object> attributes = new HashMap<String, Object>();
+        attributes.put("link", link);
+        attributes.put("linkExpiration", expirationInMinutes);
+
+        send("emailVerificationSubject", "email-verification.ftl", attributes);
+    }
+
+    private void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
+        try {
+            ExtendingThemeManager themeManager = new ExtendingThemeManager(session);
+            Theme theme = themeManager.createTheme(realm.getAccountTheme(), Theme.Type.EMAIL);
+
+            String subject =  theme.getMessages().getProperty(subjectKey);
+            String body = FreeMarkerUtil.processTemplate(attributes, template, theme);
+
+            send(subject, body);
+        } catch (Exception e) {
+            throw new EmailException("Failed to template email", e);
+        }
+    }
+
+
+    private void send(String subject, String body) throws EmailException {
+        try {
+            String address = user.getEmail();
+            Map<String, String> config = realm.getSmtpConfig();
+
+            Properties props = new Properties();
+            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")) {
+                props.setProperty("mail.smtp.port", config.get("port"));
+            }
+
+            if (auth) {
+                props.put("mail.smtp.auth", "true");
+            }
+
+            if (ssl) {
+                props.put("mail.smtp.socketFactory.port", config.get("port"));
+                props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
+            }
+
+            if (starttls) {
+                props.put("mail.smtp.starttls.enable", "true");
+            }
+
+            String from = config.get("from");
+
+            Session session = Session.getInstance(props);
+
+            Message msg = new MimeMessage(session);
+            msg.setFrom(new InternetAddress(from));
+            msg.setHeader("To", address);
+            msg.setSubject(subject);
+            msg.setText(body);
+            msg.saveChanges();
+
+            Transport transport = session.getTransport("smtp");
+            if (auth) {
+                transport.connect(config.get("user"), config.get("password"));
+            } else {
+                transport.connect();
+            }
+            transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)});
+        } catch (Exception e) {
+            log.warn("Failed to send email", e);
+            throw new EmailException(e);
+        }
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java
new file mode 100644
index 0000000..b98d981
--- /dev/null
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java
@@ -0,0 +1,31 @@
+package org.keycloak.email.freemarker;
+
+import org.keycloak.Config;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.email.EmailProviderFactory;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerEmailProviderFactory implements EmailProviderFactory {
+
+    @Override
+    public EmailProvider create(ProviderSession providerSession) {
+        return new FreeMarkerEmailProvider(providerSession);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public String getId() {
+        return "freemarker";
+    }
+
+}
diff --git a/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory b/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory
new file mode 100644
index 0000000..5b31f6e
--- /dev/null
+++ b/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory
@@ -0,0 +1 @@
+org.keycloak.email.freemarker.FreeMarkerEmailProviderFactory
\ No newline at end of file
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index be59cde..c686cf9 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
@@ -1,14 +1,59 @@
-package org.keycloak.login;
-
-import org.keycloak.models.RealmModel;
-
-import javax.ws.rs.core.UriInfo;
-
-/**
- * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
- */
-public interface LoginFormsProvider {
-
-    public LoginForms createForms(RealmModel realm, UriInfo uriInfo);
-
-}
+package org.keycloak.login;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.Provider;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface LoginFormsProvider extends Provider {
+
+    public LoginFormsProvider setRealm(RealmModel realm);
+
+    public LoginFormsProvider setUriInfo(UriInfo uriInfo);
+
+    public Response createResponse(UserModel.RequiredAction action);
+
+    public Response createLogin();
+
+    public Response createPasswordReset();
+
+    public Response createLoginTotp();
+
+    public Response createRegistration();
+
+    public Response createErrorPage();
+
+    public Response createOAuthGrant();
+
+    public Response createCode();
+
+    public LoginFormsProvider setAccessCode(String accessCodeId, String accessCode);
+
+    public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
+
+    public LoginFormsProvider setError(String message);
+
+    public LoginFormsProvider setSuccess(String message);
+
+    public LoginFormsProvider setWarning(String message);
+
+    public LoginFormsProvider setUser(UserModel user);
+
+    public LoginFormsProvider setClient(ClientModel client);
+
+    public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams);
+
+    public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData);
+
+    public LoginFormsProvider setStatus(Response.Status status);
+
+}
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java
new file mode 100644
index 0000000..e3fffea
--- /dev/null
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java
@@ -0,0 +1,25 @@
+package org.keycloak.login;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LoginFormsSpi implements Spi {
+    @Override
+    public String getName() {
+        return "login-forms";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return LoginFormsProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return LoginFormsProviderFactory.class;
+    }
+}
diff --git a/forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..afbdfbf
--- /dev/null
+++ b/forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1 @@
+org.keycloak.login.LoginFormsSpi
\ No newline at end of file
diff --git a/forms/login-freemarker/pom.xml b/forms/login-freemarker/pom.xml
index c9e85bf..9e51232 100755
--- a/forms/login-freemarker/pom.xml
+++ b/forms/login-freemarker/pom.xml
@@ -32,6 +32,12 @@
 			<version>${project.version}</version>
             <scope>provided</scope>
 		</dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-email-api</artifactId>
+			<version>${project.version}</version>
+            <scope>provided</scope>
+		</dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-model-api</artifactId>
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 5f0b0c5..1dfcd29 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -1,19 +1,314 @@
-package org.keycloak.login.freemarker;
-
-import org.keycloak.login.LoginForms;
-import org.keycloak.login.LoginFormsProvider;
-import org.keycloak.models.RealmModel;
-
-import javax.ws.rs.core.UriInfo;
-
-/**
- * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
- */
-public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
-
-    @Override
-    public LoginForms createForms(RealmModel realm, UriInfo uriInfo) {
-        return new FreeMarkerLoginForms(realm, uriInfo);
-    }
-
-}
+package org.keycloak.login.freemarker;
+
+import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.freemarker.ExtendingThemeManager;
+import org.keycloak.freemarker.FreeMarkerException;
+import org.keycloak.freemarker.FreeMarkerUtil;
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.login.LoginFormsPages;
+import org.keycloak.login.freemarker.model.CodeBean;
+import org.keycloak.login.freemarker.model.LoginBean;
+import org.keycloak.login.freemarker.model.MessageBean;
+import org.keycloak.login.freemarker.model.OAuthGrantBean;
+import org.keycloak.login.freemarker.model.ProfileBean;
+import org.keycloak.login.freemarker.model.RealmBean;
+import org.keycloak.login.freemarker.model.RegisterBean;
+import org.keycloak.login.freemarker.model.SocialBean;
+import org.keycloak.login.freemarker.model.TotpBean;
+import org.keycloak.login.freemarker.model.UrlBean;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderSession;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.flows.Urls;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
+
+    private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
+
+    private String message;
+    private String accessCodeId;
+    private String accessCode;
+    private Response.Status status = Response.Status.OK;
+    private List<RoleModel> realmRolesRequested;
+    private MultivaluedMap<String, RoleModel> resourceRolesRequested;
+    private MultivaluedMap<String, String> queryParams;
+
+    public static enum MessageType {SUCCESS, WARNING, ERROR}
+
+    private MessageType messageType = MessageType.ERROR;
+
+    private MultivaluedMap<String, String> formData;
+
+    private ProviderSession session;
+    private RealmModel realm;
+
+    private UserModel user;
+
+    private ClientModel client;
+
+    private UriInfo uriInfo;
+
+    public FreeMarkerLoginFormsProvider(ProviderSession session) {
+        this.session = session;
+    }
+
+    public LoginFormsProvider setRealm(RealmModel realm) {
+        this.realm = realm;
+        return this;
+    }
+
+    public LoginFormsProvider setUriInfo(UriInfo uriInfo) {
+        this.uriInfo = uriInfo;
+        return this;
+    }
+
+    public Response createResponse(UserModel.RequiredAction action) {
+        String actionMessage;
+        LoginFormsPages page;
+
+        switch (action) {
+            case CONFIGURE_TOTP:
+                actionMessage = Messages.ACTION_WARN_TOTP;
+                page = LoginFormsPages.LOGIN_CONFIG_TOTP;
+                break;
+            case UPDATE_PROFILE:
+                actionMessage = Messages.ACTION_WARN_PROFILE;
+                page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
+                break;
+            case UPDATE_PASSWORD:
+                actionMessage = Messages.ACTION_WARN_PASSWD;
+                page = LoginFormsPages.LOGIN_UPDATE_PASSWORD;
+                break;
+            case VERIFY_EMAIL:
+                try {
+                    UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
+                    builder.queryParam("key", accessCodeId);
+
+                    String link = builder.build(realm.getName()).toString();
+                    long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
+
+                    session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration);
+                } catch (EmailException e) {
+                    logger.error("Failed to send verification email", e);
+                    return setError("emailSendError").createErrorPage();
+                }
+
+                actionMessage = Messages.ACTION_WARN_EMAIL;
+                page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
+                break;
+            default:
+                return Response.serverError().build();
+        }
+
+        if (message == null) {
+            setWarning(actionMessage);
+        }
+
+        return createResponse(page);
+    }
+
+    private Response createResponse(LoginFormsPages page) {
+        MultivaluedMap<String, String> queryParameterMap = queryParams != null ? queryParams : uriInfo.getQueryParameters();
+
+        String requestURI = uriInfo.getBaseUri().getPath();
+        UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
+
+        for (String k : queryParameterMap.keySet()) {
+
+            Object[] objects = queryParameterMap.get(k).toArray();
+            if (objects.length == 1 && objects[0] == null) continue; //
+            uriBuilder.replaceQueryParam(k, objects);
+        }
+
+        if (accessCode != null) {
+            uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
+        }
+
+        Map<String, Object> attributes = new HashMap<String, Object>();
+
+        ExtendingThemeManager themeManager = new ExtendingThemeManager(session);
+        Theme theme;
+        try {
+            theme = themeManager.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
+        } catch (IOException e) {
+            logger.error("Failed to create theme", e);
+            return Response.serverError().build();
+        }
+
+        try {
+            attributes.put("properties", theme.getProperties());
+        } catch (IOException e) {
+            logger.warn("Failed to load properties", e);
+        }
+
+        Properties messages;
+        try {
+            messages = theme.getMessages();
+            attributes.put("rb", messages);
+        } catch (IOException e) {
+            logger.warn("Failed to load messages", e);
+            messages = new Properties();
+        }
+
+        if (message != null) {
+            attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
+        }
+        if (page == LoginFormsPages.OAUTH_GRANT) {
+            // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
+            uriBuilder.replaceQuery(null);
+        }
+        URI baseUri = uriBuilder.build();
+
+        if (realm != null) {
+            attributes.put("realm", new RealmBean(realm));
+            attributes.put("social", new SocialBean(realm, baseUri));
+            attributes.put("url", new UrlBean(realm, theme, baseUri));
+        }
+
+        attributes.put("login", new LoginBean(formData));
+
+        switch (page) {
+            case LOGIN_CONFIG_TOTP:
+                attributes.put("totp", new TotpBean(realm, user, baseUri));
+                break;
+            case LOGIN_UPDATE_PROFILE:
+                attributes.put("user", new ProfileBean(user));
+                break;
+            case REGISTER:
+                attributes.put("register", new RegisterBean(formData));
+                break;
+            case OAUTH_GRANT:
+                attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested));
+                break;
+            case CODE:
+                attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
+                break;
+        }
+
+        try {
+            String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
+            return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
+        } catch (FreeMarkerException e) {
+            logger.error("Failed to process template", e);
+            return Response.serverError().build();
+        }
+    }
+
+    public Response createLogin() {
+        return createResponse(LoginFormsPages.LOGIN);
+    }
+
+    public Response createPasswordReset() {
+        return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
+    }
+
+    public Response createLoginTotp() {
+        return createResponse(LoginFormsPages.LOGIN_TOTP);
+    }
+
+    public Response createRegistration() {
+        return createResponse(LoginFormsPages.REGISTER);
+    }
+
+    public Response createErrorPage() {
+        setStatus(Response.Status.INTERNAL_SERVER_ERROR);
+        return createResponse(LoginFormsPages.ERROR);
+    }
+
+    public Response createOAuthGrant() {
+        return createResponse(LoginFormsPages.OAUTH_GRANT);
+    }
+
+    @Override
+    public Response createCode() {
+        return createResponse(LoginFormsPages.CODE);
+    }
+
+    public FreeMarkerLoginFormsProvider setError(String message) {
+        this.message = message;
+        this.messageType = MessageType.ERROR;
+        return this;
+    }
+
+    public FreeMarkerLoginFormsProvider setSuccess(String message) {
+        this.message = message;
+        this.messageType = MessageType.SUCCESS;
+        return this;
+    }
+
+    public FreeMarkerLoginFormsProvider setWarning(String message) {
+        this.message = message;
+        this.messageType = MessageType.WARNING;
+        return this;
+    }
+
+    public FreeMarkerLoginFormsProvider setUser(UserModel user) {
+        this.user = user;
+        return this;
+    }
+
+    public FreeMarkerLoginFormsProvider setClient(ClientModel client) {
+        this.client = client;
+        return this;
+    }
+
+    public FreeMarkerLoginFormsProvider setFormData(MultivaluedMap<String, String> formData) {
+        this.formData = formData;
+        return this;
+    }
+
+    @Override
+    public LoginFormsProvider setAccessCode(String accessCodeId, String accessCode) {
+        this.accessCodeId = accessCodeId;
+        this.accessCode = accessCode;
+        return this;
+    }
+
+    @Override
+    public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
+        this.realmRolesRequested = realmRolesRequested;
+        this.resourceRolesRequested = resourceRolesRequested;
+        return this;
+    }
+
+    @Override
+    public LoginFormsProvider setStatus(Response.Status status) {
+        this.status = status;
+        return this;
+    }
+
+    @Override
+    public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) {
+        this.queryParams = queryParams;
+        return this;
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java
new file mode 100755
index 0000000..6b11d19
--- /dev/null
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java
@@ -0,0 +1,31 @@
+package org.keycloak.login.freemarker;
+
+import org.keycloak.Config;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.login.LoginFormsProviderFactory;
+import org.keycloak.provider.ProviderSession;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerLoginFormsProviderFactory implements LoginFormsProviderFactory {
+
+    @Override
+    public LoginFormsProvider create(ProviderSession providerSession) {
+        return new FreeMarkerLoginFormsProvider(providerSession);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public String getId() {
+        return "freemarker";
+    }
+
+}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java
index a6b36d4..72d48b7 100644
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java
@@ -21,7 +21,7 @@
  */
 package org.keycloak.login.freemarker.model;
 
-import org.keycloak.login.freemarker.FreeMarkerLoginForms;
+import org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -30,9 +30,9 @@ public class MessageBean {
 
     private String summary;
 
-    private FreeMarkerLoginForms.MessageType type;
+    private FreeMarkerLoginFormsProvider.MessageType type;
 
-    public MessageBean(String message, FreeMarkerLoginForms.MessageType type) {
+    public MessageBean(String message, FreeMarkerLoginFormsProvider.MessageType type) {
         this.summary = message;
         this.type = type;
     }
@@ -46,15 +46,15 @@ public class MessageBean {
     }
 
     public boolean isSuccess() {
-        return FreeMarkerLoginForms.MessageType.SUCCESS.equals(this.type);
+        return FreeMarkerLoginFormsProvider.MessageType.SUCCESS.equals(this.type);
     }
 
     public boolean isWarning() {
-        return FreeMarkerLoginForms.MessageType.WARNING.equals(this.type);
+        return FreeMarkerLoginFormsProvider.MessageType.WARNING.equals(this.type);
     }
 
     public boolean isError() {
-        return FreeMarkerLoginForms.MessageType.ERROR.equals(this.type);
+        return FreeMarkerLoginFormsProvider.MessageType.ERROR.equals(this.type);
     }
 
 }
\ No newline at end of file
diff --git a/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory
new file mode 100644
index 0000000..893783b
--- /dev/null
+++ b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory
@@ -0,0 +1 @@
+org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory
\ No newline at end of file

forms/pom.xml 2(+2 -0)

diff --git a/forms/pom.xml b/forms/pom.xml
index f70ac61..0f2268d 100755
--- a/forms/pom.xml
+++ b/forms/pom.xml
@@ -19,6 +19,8 @@
         <module>common-themes</module>
         <module>account-api</module>
         <module>account-freemarker</module>
+        <module>email-api</module>
+        <module>email-freemarker</module>
         <module>login-api</module>
         <module>login-freemarker</module>
     </modules>

pom.xml 5(+5 -0)

diff --git a/pom.xml b/pom.xml
index 335383b..0aa3dff 100755
--- a/pom.xml
+++ b/pom.xml
@@ -122,6 +122,11 @@
                 <version>2.3.8</version>
             </dependency>
             <dependency>
+                <groupId>javax.mail</groupId>
+                <artifactId>mail</artifactId>
+                <version>1.4.7</version>
+            </dependency>
+            <dependency>
                 <groupId>org.jboss.resteasy</groupId>
                 <artifactId>jaxrs-api</artifactId>
                 <version>${resteasy.version}</version>
diff --git a/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java b/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java
index d8ef5e0..650af91 100755
--- a/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java
+++ b/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java
@@ -59,4 +59,8 @@ public class AerogearThemeProvider implements ThemeProvider {
         return nameSet(type).contains(name);
     }
 
+    @Override
+    public void close() {
+    }
+
 }

server/pom.xml 10(+10 -0)

diff --git a/server/pom.xml b/server/pom.xml
index 1276b48..e353dd5 100755
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -119,6 +119,16 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-login-api</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/server/src/main/resources/META-INF/keycloak-server.json b/server/src/main/resources/META-INF/keycloak-server.json
index 62405d9..544ad81 100644
--- a/server/src/main/resources/META-INF/keycloak-server.json
+++ b/server/src/main/resources/META-INF/keycloak-server.json
@@ -20,6 +20,18 @@
         "dir": "${jboss.server.config.dir}/themes"
     },
 
+    "login-forms": {
+        "provider": "freemarker"
+    },
+
+    "account": {
+        "provider": "freemarker"
+    },
+
+    "email": {
+        "provider": "freemarker"
+    },
+
     "scheduled": {
         "interval": 900
     }

services/pom.xml 6(+6 -0)

diff --git a/services/pom.xml b/services/pom.xml
index 6b8ceef..7612a20 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -51,6 +51,12 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-login-api</artifactId>
             <version>${project.version}</version>
             <scope>provided</scope>
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 453f550..c23858d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -347,7 +347,7 @@ public class AuthenticationManager {
 
     private boolean checkEnabled(UserModel user) {
         if (!user.isEnabled()) {
-            logger.warn("Account is disabled, contact admin. " + user.getLoginName());
+            logger.warn("AccountProvider is disabled, contact admin. " + user.getLoginName());
             return false;
         } else {
             return true;
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 85a1461..5f6206f 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -25,14 +25,16 @@ import org.jboss.logging.Logger;
 import org.jboss.resteasy.spi.BadRequestException;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.OAuth2Constants;
-import org.keycloak.account.Account;
-import org.keycloak.account.AccountLoader;
 import org.keycloak.account.AccountPages;
+import org.keycloak.account.AccountProvider;
 import org.keycloak.audit.Audit;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
 import org.keycloak.audit.Events;
+import org.keycloak.authentication.AuthProviderStatus;
+import org.keycloak.authentication.AuthenticationProviderException;
+import org.keycloak.authentication.AuthenticationProviderManager;
 import org.keycloak.models.AccountRoles;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.AuthenticationLinkModel;
@@ -44,8 +46,8 @@ import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.TimeBasedOTP;
-import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.provider.ProviderSession;
+import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.ForbiddenException;
 import org.keycloak.services.managers.AppAuthManager;
 import org.keycloak.services.managers.Auth;
@@ -62,9 +64,6 @@ import org.keycloak.services.validation.Validation;
 import org.keycloak.social.SocialLoader;
 import org.keycloak.social.SocialProvider;
 import org.keycloak.social.SocialProviderException;
-import org.keycloak.authentication.AuthProviderStatus;
-import org.keycloak.authentication.AuthenticationProviderException;
-import org.keycloak.authentication.AuthenticationProviderManager;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
@@ -76,7 +75,6 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.NewCookie;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
@@ -111,8 +109,6 @@ public class AccountService {
         AUDIT_DETAILS.add(Details.AUTH_METHOD);
     }
 
-    public static final String KEYCLOAK_ACCOUNT_IDENTITY_COOKIE = "KEYCLOAK_ACCOUNT_IDENTITY";
-
     private RealmModel realm;
 
     @Context
@@ -131,7 +127,7 @@ public class AccountService {
     private final ApplicationModel application;
     private Audit audit;
     private final SocialRequestManager socialRequestManager;
-    private Account account;
+    private AccountProvider account;
     private Auth auth;
     private AuditProvider auditProvider;
 
@@ -146,7 +142,7 @@ public class AccountService {
     public void init() {
         auditProvider = providers.getProvider(AuditProvider.class);
 
-        account = AccountLoader.load().createAccount(uriInfo).setRealm(realm);
+        account = providers.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(uriInfo);
 
         boolean passwordUpdateSupported = false;
         AuthenticationManager.AuthResult authResult = authManager.authenticateRequest(realm, uriInfo, headers);
@@ -181,7 +177,7 @@ public class AccountService {
             try {
                 require(AccountRoles.MANAGE_ACCOUNT);
             } catch (ForbiddenException e) {
-                return Flows.forms(realm, uriInfo).setError("No access").createErrorPage();
+                return Flows.forms(providers, realm, uriInfo).setError("No access").createErrorPage();
             }
 
             String[] referrer = getReferrer();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index eaf7666..bd897fa 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
@@ -6,8 +6,9 @@ import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.jboss.resteasy.spi.HttpResponse;
 import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.freemarker.ExtendingThemeManager;
 import org.keycloak.freemarker.Theme;
-import org.keycloak.freemarker.ThemeLoader;
+import org.keycloak.freemarker.ThemeProvider;
 import org.keycloak.models.AdminRoles;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.Constants;
@@ -280,7 +281,8 @@ public class AdminConsole {
 
         try {
             //logger.info("getting resource: " + path + " uri: " + uriInfo.getRequestUri().toString());
-            Theme theme = ThemeLoader.createTheme(realm.getAdminTheme(), Theme.Type.ADMIN);
+            ExtendingThemeManager themeManager = new ExtendingThemeManager(providerSession);
+            Theme theme = themeManager.createTheme(realm.getAdminTheme(), Theme.Type.ADMIN);
             InputStream resource = theme.getResourceAsStream(path);
             if (resource != null) {
                 String contentType = mimeTypes.getContentType(path);
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 7a089d8..014fe52 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -133,7 +133,7 @@ public class RealmAdminResource {
 
     @Path("users")
     public UsersResource users() {
-        UsersResource users = new UsersResource(realm, auth, tokenManager);
+        UsersResource users = new UsersResource(providers, realm, auth, tokenManager);
         ResteasyProviderFactory.getInstance().injectProperties(users);
         //resourceContext.initResource(users);
         return users;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index feba6f9..e4cc8c1 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -4,6 +4,8 @@ import org.jboss.logging.Logger;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.BadRequestException;
 import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
@@ -15,6 +17,7 @@ import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.adapters.action.UserStats;
 import org.keycloak.representations.idm.ApplicationMappingsRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
@@ -23,8 +26,6 @@ import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.SocialLinkRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.representations.idm.UserSessionRepresentation;
-import org.keycloak.services.email.EmailException;
-import org.keycloak.services.email.EmailSender;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.ModelToRepresentation;
 import org.keycloak.services.managers.RealmManager;
@@ -46,6 +47,7 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -53,6 +55,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -63,10 +66,12 @@ public class UsersResource {
 
     protected RealmModel realm;
 
+    private ProviderSession providerSession;
     private RealmAuth auth;
     private TokenManager tokenManager;
 
-    public UsersResource(RealmModel realm, RealmAuth auth, TokenManager tokenManager) {
+    public UsersResource(ProviderSession providerSession, RealmModel realm, RealmAuth auth, TokenManager tokenManager) {
+        this.providerSession = providerSession;
         this.auth = auth;
         this.realm = realm;
         this.tokenManager = tokenManager;
@@ -660,7 +665,7 @@ public class UsersResource {
 
         ClientModel client = realm.findClient(clientId);
         if (client == null || !client.isEnabled()) {
-            return Flows.errors().error("Account management not enabled", Response.Status.INTERNAL_SERVER_ERROR);
+            return Flows.errors().error("AccountProvider management not enabled", Response.Status.INTERNAL_SERVER_ERROR);
         }
 
         Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(user.getRequiredActions());
@@ -671,7 +676,14 @@ public class UsersResource {
         accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
 
         try {
-            new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo);
+            UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
+            builder.queryParam("key", accessCode.getId());
+
+            String link = builder.build(realm.getName()).toString();
+            long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
+
+            providerSession.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration);
+
             return Response.ok().build();
         } catch (EmailException e) {
             logger.error("Failed to send password reset email", e);
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
index 2d3aa67..388ff82 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
@@ -22,9 +22,9 @@
 package org.keycloak.services.resources.flows;
 
 import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.login.LoginForms;
-import org.keycloak.login.LoginFormsLoader;
+import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderSession;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.SocialRequestManager;
 import org.keycloak.services.managers.TokenManager;
@@ -40,13 +40,13 @@ public class Flows {
     private Flows() {
     }
 
-    public static LoginForms forms(RealmModel realm, UriInfo uriInfo) {
-        return LoginFormsLoader.load().createForms(realm, uriInfo);
+    public static LoginFormsProvider forms(ProviderSession session, RealmModel realm, UriInfo uriInfo) {
+        return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo);
     }
 
-    public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
+    public static OAuthFlows oauth(ProviderSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
             TokenManager tokenManager) {
-        return new OAuthFlows(realm, request, uriInfo, authManager, tokenManager);
+        return new OAuthFlows(session, realm, request, uriInfo, authManager, tokenManager);
     }
 
     public static SocialRedirectFlows social(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) {
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index d31aab5..2ed76b0 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
@@ -35,6 +35,7 @@ import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
@@ -56,6 +57,8 @@ public class OAuthFlows {
 
     private static final Logger log = Logger.getLogger(OAuthFlows.class);
 
+    private final ProviderSession providerSession;
+
     private final RealmModel realm;
 
     private final HttpRequest request;
@@ -66,8 +69,9 @@ public class OAuthFlows {
 
     private final TokenManager tokenManager;
 
-    OAuthFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
+    OAuthFlows(ProviderSession providerSession, RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
             TokenManager tokenManager) {
+        this.providerSession = providerSession;
         this.realm = realm;
         this.request = request;
         this.uriInfo = uriInfo;
@@ -84,7 +88,7 @@ public class OAuthFlows {
         String code = accessCode.getCode();
 
         if (Constants.INSTALLED_APP_URN.equals(redirect)) {
-            return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), code).createCode();
+            return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), code).createCode();
         } else {
             UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code);
             log.debugv("redirectAccessCode: state: {0}", state);
@@ -102,7 +106,7 @@ public class OAuthFlows {
 
     public Response redirectError(ClientModel client, String error, String state, String redirect) {
         if (Constants.INSTALLED_APP_URN.equals(redirect)) {
-            return Flows.forms(realm, uriInfo).setError(error).createCode();
+            return Flows.forms(providerSession, realm, uriInfo).setError(error).createCode();
         } else {
             UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error);
             if (state != null) {
@@ -139,14 +143,14 @@ public class OAuthFlows {
                 audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
             }
 
-            return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
+            return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
                     .createResponse(action);
         }
 
         if (!isResource
                 && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
             accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
-            return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).
+            return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).
                     setAccessRequest(accessCode.getRealmRolesRequested(), accessCode.getResourceRolesRequested()).
                     setClient(client).createOAuthGrant();
         }
@@ -160,7 +164,7 @@ public class OAuthFlows {
     }
 
     public Response forwardToSecurityFailure(String message) {
-        return Flows.forms(realm, uriInfo).setError(message).createErrorPage();
+        return Flows.forms(providerSession, realm, uriInfo).setError(message).createErrorPage();
     }
 
     private void isTotpConfigurationRequired(UserModel user) {
diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index 4c6139c..4c1e062 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -28,7 +28,9 @@ import org.keycloak.audit.Audit;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Errors;
 import org.keycloak.audit.Events;
-import org.keycloak.login.LoginForms;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.models.ClientModel;
@@ -41,13 +43,12 @@ import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.provider.ProviderSession;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.ClientConnection;
-import org.keycloak.services.email.EmailException;
-import org.keycloak.services.email.EmailSender;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.services.resources.flows.Urls;
 import org.keycloak.services.validation.Validation;
 import org.keycloak.authentication.AuthenticationProviderException;
 import org.keycloak.authentication.AuthenticationProviderManager;
@@ -62,12 +63,12 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import javax.ws.rs.ext.Providers;
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -120,7 +121,7 @@ public class RequiredActionsService {
 
         String error = Validation.validateUpdateProfileForm(formData);
         if (error != null) {
-            return Flows.forms(realm, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE);
+            return Flows.forms(providerSession, realm, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE);
         }
 
         user.setFirstName(formData.getFirst("firstName"));
@@ -160,7 +161,7 @@ public class RequiredActionsService {
         String totp = formData.getFirst("totp");
         String totpSecret = formData.getFirst("totpSecret");
 
-        LoginForms loginForms = Flows.forms(realm, uriInfo).setUser(user);
+        LoginFormsProvider loginForms = Flows.forms(providerSession, realm, uriInfo).setUser(user);
         if (Validation.isEmpty(totp)) {
             return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP);
         } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
@@ -201,7 +202,7 @@ public class RequiredActionsService {
         String passwordNew = formData.getFirst("password-new");
         String passwordConfirm = formData.getFirst("password-confirm");
 
-        LoginForms loginForms = Flows.forms(realm, uriInfo).setUser(user);
+        LoginFormsProvider loginForms = Flows.forms(providerSession, realm, uriInfo).setUser(user);
         if (Validation.isEmpty(passwordNew)) {
             return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD);
         } else if (!passwordNew.equals(passwordConfirm)) {
@@ -261,7 +262,7 @@ public class RequiredActionsService {
             initAudit(accessCode);
             //audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
 
-            return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser())
+            return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser())
                     .createResponse(RequiredAction.VERIFY_EMAIL);
         }
     }
@@ -277,9 +278,9 @@ public class RequiredActionsService {
                 return unauthorized();
             }
 
-            return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD);
+            return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD);
         } else {
-            return Flows.forms(realm, uriInfo).createPasswordReset();
+            return Flows.forms(providerSession, realm, uriInfo).createPasswordReset();
         }
     }
 
@@ -298,11 +299,11 @@ public class RequiredActionsService {
 
         ClientModel client = realm.findClient(clientId);
         if (client == null) {
-            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure(
+            return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure(
                     "Unknown login requester.");
         }
         if (!client.isEnabled()) {
-            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure(
+            return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure(
                     "Login requester not enabled.");
         }
 
@@ -334,15 +335,22 @@ public class RequiredActionsService {
             accessCode.setUsername(username);
 
             try {
-                new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo);
+                UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
+                builder.queryParam("key", accessCode.getId());
+
+                String link = builder.build(realm.getName()).toString();
+                long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
+
+                providerSession.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration);
+
                 audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getId()).success();
             } catch (EmailException e) {
                 logger.error("Failed to send password reset email", e);
-                return Flows.forms(realm, uriInfo).setError("emailSendError").createErrorPage();
+                return Flows.forms(providerSession, realm, uriInfo).setError("emailSendError").createErrorPage();
             }
         }
 
-        return Flows.forms(realm, uriInfo).setSuccess("emailSent").createPasswordReset();
+        return Flows.forms(providerSession, realm, uriInfo).setSuccess("emailSent").createPasswordReset();
     }
 
     private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
@@ -399,7 +407,7 @@ public class RequiredActionsService {
 
         Set<RequiredAction> requiredActions = user.getRequiredActions();
         if (!requiredActions.isEmpty()) {
-            return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
+            return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
                     .createResponse(requiredActions.iterator().next());
         } else {
             logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri());
@@ -410,13 +418,13 @@ public class RequiredActionsService {
             UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
             if (!AuthenticationManager.isSessionValid(realm, session)) {
                 AuthenticationManager.logout(realm, session, uriInfo);
-                return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri());
+                return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri());
             }
             audit.session(session);
 
             audit.success();
 
-            return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
+            return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode,
                     session, accessCode.getState(), accessCode.getRedirectUri());
         }
     }
@@ -437,7 +445,7 @@ public class RequiredActionsService {
     }
 
     private Response unauthorized() {
-        return Flows.forms(realm, uriInfo).setError("Unauthorized request").createErrorPage();
+        return Flows.forms(providerSession, realm, uriInfo).setError("Unauthorized request").createErrorPage();
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
index 06f0ae6..76c8985 100755
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -97,6 +97,9 @@ public class SocialResource {
     */
 
     @Context
+    protected ProviderSession providerSession;
+
+    @Context
     protected KeycloakSession session;
 
     @Context
@@ -133,7 +136,7 @@ public class SocialResource {
                 .detail(Details.AUTH_METHOD, "social@" + provider.getId());
 
         AuthenticationManager authManager = new AuthenticationManager(providers);
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+        OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
         if (!realm.isEnabled()) {
             audit.error(Errors.REALM_DISABLED);
@@ -177,7 +180,7 @@ public class SocialResource {
             queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType);
 
             audit.error(Errors.REJECTED_BY_USER);
-            return  Flows.forms(realm, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin();
+            return  Flows.forms(providerSession, realm, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin();
         } catch (SocialProviderException e) {
             logger.error("Failed to process social callback", e);
             return oauth.forwardToSecurityFailure("Failed to process social callback");
@@ -279,25 +282,25 @@ public class SocialResource {
         SocialProvider provider = SocialLoader.load(providerId);
         if (provider == null) {
             audit.error(Errors.SOCIAL_PROVIDER_NOT_FOUND);
-            return Flows.forms(realm, uriInfo).setError("Social provider not found").createErrorPage();
+            return Flows.forms(providerSession, realm, uriInfo).setError("Social provider not found").createErrorPage();
         }
 
         ClientModel client = realm.findClient(clientId);
         if (client == null) {
             audit.error(Errors.CLIENT_NOT_FOUND);
             logger.warn("Unknown login requester: " + clientId);
-            return Flows.forms(realm, uriInfo).setError("Unknown login requester.").createErrorPage();
+            return Flows.forms(providerSession, realm, uriInfo).setError("Unknown login requester.").createErrorPage();
         }
 
         if (!client.isEnabled()) {
             audit.error(Errors.CLIENT_DISABLED);
             logger.warn("Login requester not enabled.");
-            return Flows.forms(realm, uriInfo).setError("Login requester not enabled.").createErrorPage();
+            return Flows.forms(providerSession, realm, uriInfo).setError("Login requester not enabled.").createErrorPage();
         }
         redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, client);
         if (redirectUri == null) {
             audit.error(Errors.INVALID_REDIRECT_URI);
-            return Flows.forms(realm, uriInfo).setError("Invalid redirect_uri.").createErrorPage();
+            return Flows.forms(providerSession, realm, uriInfo).setError("Invalid redirect_uri.").createErrorPage();
         }
 
         try {
@@ -308,7 +311,7 @@ public class SocialResource {
                     .putClientAttribute("responseType", responseType).redirectToSocialProvider();
         } catch (Throwable t) {
             logger.error("Failed to redirect to social auth", t);
-            return Flows.forms(realm, uriInfo).setError("Failed to redirect to social auth").createErrorPage();
+            return Flows.forms(providerSession, realm, uriInfo).setError("Failed to redirect to social auth").createErrorPage();
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
index cc04a57..1d4fe1c 100755
--- a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
@@ -1,14 +1,17 @@
 package org.keycloak.services.resources;
 
 import org.jboss.logging.Logger;
+import org.keycloak.freemarker.ExtendingThemeManager;
 import org.keycloak.freemarker.Theme;
-import org.keycloak.freemarker.ThemeLoader;
+import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.provider.ProviderSession;
 
 import javax.activation.FileTypeMap;
 import javax.activation.MimetypesFileTypeMap;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import java.io.InputStream;
 
@@ -22,11 +25,15 @@ public class ThemeResource {
 
     private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap();
 
+    @Context
+    private ProviderSession providerSession;
+
     @GET
     @Path("/{themType}/{themeName}/{path:.*}")
     public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) {
         try {
-            Theme theme = ThemeLoader.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase()));
+            ExtendingThemeManager themeManager = new ExtendingThemeManager(providerSession);
+            Theme theme = themeManager.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase()));
             InputStream resource = theme.getResourceAsStream(path);
             if (resource != null) {
                 return Response.ok(resource).type(mimeTypes.getContentType(path)).build();
@@ -39,5 +46,4 @@ public class ThemeResource {
         }
     }
 
-
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index d9c12de..9b47de5 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -242,14 +242,14 @@ public class TokenService {
             case ACTIONS_REQUIRED:
                 err = new HashMap<String, String>();
                 err.put(OAuth2Constants.ERROR, "invalid_grant");
-                err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account temporarily disabled");
+                err.put(OAuth2Constants.ERROR_DESCRIPTION, "AccountProvider temporarily disabled");
                 audit.error(Errors.USER_TEMPORARILY_DISABLED);
                 return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
                         .build();
             case ACCOUNT_DISABLED:
                 err = new HashMap<String, String>();
                 err.put(OAuth2Constants.ERROR, "invalid_grant");
-                err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account disabled");
+                err.put(OAuth2Constants.ERROR_DESCRIPTION, "AccountProvider disabled");
                 audit.error(Errors.USER_DISABLED);
                 return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
                         .build();
@@ -340,7 +340,7 @@ public class TokenService {
             audit.detail(Details.REMEMBER_ME, "true");
         }
 
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+        OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
         if (!checkSsl()) {
             return oauth.forwardToSecurityFailure("HTTPS required");
@@ -391,18 +391,18 @@ public class TokenService {
                 return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, username, remember, "form", audit);
             case ACCOUNT_TEMPORARILY_DISABLED:
                 audit.error(Errors.USER_TEMPORARILY_DISABLED);
-                return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin();
+                return Flows.forms(providerSession, realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin();
             case ACCOUNT_DISABLED:
                 audit.error(Errors.USER_DISABLED);
-                return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
+                return Flows.forms(providerSession, realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
             case MISSING_TOTP:
-                return Flows.forms(realm, uriInfo).setFormData(formData).createLoginTotp();
+                return Flows.forms(providerSession, realm, uriInfo).setFormData(formData).createLoginTotp();
             case INVALID_USER:
                 audit.error(Errors.USER_NOT_FOUND);
-                return Flows.forms(realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
+                return Flows.forms(providerSession, realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
             default:
                 audit.error(Errors.INVALID_USER_CREDENTIALS);
-                return Flows.forms(realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
+                return Flows.forms(providerSession, realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
         }
     }
 
@@ -432,7 +432,7 @@ public class TokenService {
                 .detail(Details.EMAIL, email)
                 .detail(Details.REGISTER_METHOD, "form");
 
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+        OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
         if (!realm.isEnabled()) {
             logger.warn("Realm not enabled");
@@ -477,7 +477,7 @@ public class TokenService {
 
         if (error != null) {
             audit.error(Errors.INVALID_REGISTRATION);
-            return Flows.forms(realm, uriInfo).setError(error).setFormData(formData).createRegistration();
+            return Flows.forms(providerSession, realm, uriInfo).setError(error).setFormData(formData).createRegistration();
         }
 
         AuthenticationProviderManager authenticationProviderManager = AuthenticationProviderManager.getManager(realm, providerSession);
@@ -485,7 +485,7 @@ public class TokenService {
         // Validate that user with this username doesn't exist in realm or any authentication provider
         if (realm.getUser(username) != null || authenticationProviderManager.getUser(username) != null) {
             audit.error(Errors.USERNAME_IN_USE);
-            return Flows.forms(realm, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration();
+            return Flows.forms(providerSession, realm, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration();
         }
 
         UserModel user = realm.addUser(username);
@@ -513,7 +513,7 @@ public class TokenService {
             // User already registered, but force him to update password
             if (!passwordUpdateSuccessful) {
                 user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
-                return Flows.forms(realm, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
+                return Flows.forms(providerSession, realm, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
             }
         }
 
@@ -722,7 +722,7 @@ public class TokenService {
 
         audit.event(Events.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code");
 
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+        OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
         if (!checkSsl()) {
             return oauth.forwardToSecurityFailure("HTTPS required");
@@ -766,7 +766,7 @@ public class TokenService {
             return oauth.redirectError(client, "access_denied", state, redirect);
         }
         logger.info("createLogin() now...");
-        return Flows.forms(realm, uriInfo).createLogin();
+        return Flows.forms(providerSession, realm, uriInfo).createLogin();
     }
 
     @Path("registrations")
@@ -778,7 +778,7 @@ public class TokenService {
 
         audit.event(Events.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code");
 
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+        OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
         if (!checkSsl()) {
             return oauth.forwardToSecurityFailure("HTTPS required");
@@ -816,7 +816,7 @@ public class TokenService {
 
         authManager.expireIdentityCookie(realm, uriInfo);
 
-        return Flows.forms(realm, uriInfo).createRegistration();
+        return Flows.forms(providerSession, realm, uriInfo).createRegistration();
     }
 
     @Path("logout")
@@ -868,7 +868,7 @@ public class TokenService {
     public Response processOAuth(final MultivaluedMap<String, String> formData) {
         audit.event(Events.LOGIN).detail(Details.RESPONSE_TYPE, "code");
 
-        OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager);
+        OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
         if (!checkSsl()) {
             return oauth.forwardToSecurityFailure("HTTPS required");
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index f54b269..7924ae9 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -198,6 +198,16 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-account-api</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json
index ee6c8fc..6b4cd35 100644
--- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json
@@ -20,6 +20,18 @@
         "dir": "${keycloak.theme.dir}"
     },
 
+    "login-forms": {
+        "provider": "freemarker"
+    },
+
+    "account": {
+        "provider": "freemarker"
+    },
+
+    "email": {
+        "provider": "freemarker"
+    },
+
     "scheduled": {
         "interval": 900
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index 974ae88..9746666 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -33,6 +33,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.org.keycloak.testsuite.util.MailUtil;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.AppPage.RequestType;
 import org.keycloak.testsuite.pages.LoginPage;
@@ -113,12 +114,7 @@ public class RequiredActionEmailVerificationTest {
         MimeMessage message = greenMail.getReceivedMessages()[0];
 
         String body = (String) message.getContent();
-
-        Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*");
-        Matcher m = p.matcher(body);
-        m.matches();
-
-        String verificationUrl = m.group(1);
+        String verificationUrl = MailUtil.getLink(body);
 
         Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
         String sessionId = sendEvent.getSessionId();
@@ -152,16 +148,12 @@ public class RequiredActionEmailVerificationTest {
 
         String body = (String) message.getContent();
 
-        Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*");
-        Matcher m = p.matcher(body);
-        m.matches();
-
         Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
         String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
 
-        String verificationUrl = m.group(1);
+        String verificationUrl = MailUtil.getLink(body);
 
         driver.navigate().to(verificationUrl.trim());
 
@@ -194,13 +186,9 @@ public class RequiredActionEmailVerificationTest {
 
         String body = (String) message.getContent();
 
-        Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*");
-        Matcher m = p.matcher(body);
-        m.matches();
-
         events.expectRequiredAction("send_verify_email").session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
 
-        String verificationUrl = m.group(1);
+        String verificationUrl = MailUtil.getLink(body);
 
         driver.navigate().to(verificationUrl.trim());
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index e2259f9..c1066bb 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -35,6 +35,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.org.keycloak.testsuite.util.MailUtil;
 import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.AppPage.RequestType;
 import org.keycloak.testsuite.pages.LoginPage;
@@ -133,7 +134,7 @@ public class ResetPasswordTest {
         MimeMessage message = greenMail.getReceivedMessages()[0];
 
         String body = (String) message.getContent();
-        String changePasswordUrl = body.split("\n")[3];
+        String changePasswordUrl = MailUtil.getLink(body);
 
         driver.navigate().to(changePasswordUrl.trim());
 
@@ -205,7 +206,7 @@ public class ResetPasswordTest {
         MimeMessage message = greenMail.getReceivedMessages()[0];
 
         String body = (String) message.getContent();
-        String changePasswordUrl = body.split("\n")[3];
+        String changePasswordUrl = MailUtil.getLink(body);
 
         String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java
new file mode 100644
index 0000000..440fdb8
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java
@@ -0,0 +1,21 @@
+package org.keycloak.testsuite.org.keycloak.testsuite.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class MailUtil {
+
+    private static Pattern mailPattern = Pattern.compile("http[^\\s]*");
+
+    public static String getLink(String body) {
+        Matcher matcher = mailPattern.matcher(body);
+        if (matcher.find()) {
+            return matcher.group();
+        }
+        throw new AssertionError("No link found in " + body);
+    }
+
+}