org.entcore.common.notification.TimelineMailer.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.common.notification.TimelineMailer.java

Source

/*
 * Copyright  WebServices pour l'ducation, 2016
 *
 * This file is part of ENT Core. ENT Core is a versatile ENT engine based on the JVM.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation (version 3 of the License).
 *
 * For the sake of explanation, any module that communicate over native
 * Web protocols, such as HTTP, with ENT Core is outside the scope of this
 * license and could be license under its own terms. This is merely considered
 * normal use of ENT Core, and does not fall under the heading of "covered work".
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */

package org.entcore.common.notification;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.entcore.common.email.EmailFactory;
import org.entcore.common.http.request.JsonHttpServerRequest;
import org.entcore.common.neo4j.Neo4j;
import org.entcore.common.neo4j.Neo4jResult;
import org.vertx.java.core.Handler;
import org.vertx.java.core.Vertx;
import org.vertx.java.core.VoidHandler;
import org.vertx.java.core.eventbus.EventBus;
import org.vertx.java.core.eventbus.Message;
import org.vertx.java.core.http.HttpServerRequest;
import org.vertx.java.core.json.JsonArray;
import org.vertx.java.core.json.JsonObject;
import org.vertx.java.core.logging.Logger;
import org.vertx.java.core.logging.impl.LoggerFactory;
import org.vertx.java.platform.Container;

import com.mongodb.QueryBuilder;

import fr.wseduc.mongodb.MongoDb;
import fr.wseduc.mongodb.MongoQueryBuilder;
import fr.wseduc.webutils.Either;
import fr.wseduc.webutils.I18n;
import fr.wseduc.webutils.email.EmailSender;

public class TimelineMailer {

    private static final String TIMELINE_ADDRESS = "wse.timeline";
    private static final String USERBOOK_ADDRESS = "userbook.preferences";
    private final EventBus eb;
    private final Vertx vertx;
    private final EmailSender emailSender;
    private final Neo4j neo4j = Neo4j.getInstance();
    private final MongoDb mongo = MongoDb.getInstance();
    private static final Logger log = LoggerFactory.getLogger(TimelineMailer.class);

    private final int USERS_LIMIT;

    public TimelineMailer(Vertx vertx, EventBus eb, Container container) {
        this(vertx, eb, container, 10);
    }

    public TimelineMailer(Vertx vertx, EventBus eb, Container container, int usersLimit) {
        this.vertx = vertx;
        this.eb = eb;
        this.USERS_LIMIT = usersLimit;
        EmailFactory emailFactory = new EmailFactory(this.vertx, container, container.config());
        emailSender = emailFactory.getSender();
    }

    /**
     * Translates a key using the usual i18n keys + the timeline folders keys (from all apps).
     *
     * @param keys : Keys to translate
     * @param domain : Domain of the i18n files
     * @param language : Language preference of the receiver
     * @param handler : Handles a JsonArray containing translated Strings.
     */
    private void translateTimeline(JsonArray keys, String domain, String language,
            final Handler<JsonArray> handler) {
        eb.send(TIMELINE_ADDRESS,
                new JsonObject().putString("action", "translate-timeline").putString("language", language)
                        .putString("domain", domain).putArray("i18nKeys", keys),
                new Handler<Message<JsonObject>>() {
                    public void handle(Message<JsonObject> event) {
                        handler.handle(event.body().getArray("translations", new JsonArray()));
                    }
                });
    }

    /**
     * Processes a mustache template using lambdas defined in the Timeline module.
     *
     * @param parameters : Template parameters
     * @param notificationName : Name of the notification (mostly useless)
     * @param template : Template contents
     * @param domain : Domain of the i18n files
     * @param language : Language preference of the receiver
     * @param handler : Handles the processed template
     */
    private void processTimelineTemplate(JsonObject parameters, String notificationName, String template,
            String domain, String scheme, String language, final Handler<String> handler) {
        eb.send(TIMELINE_ADDRESS,
                new JsonObject().putString("action", "process-timeline-template")
                        .putObject("request", new JsonObject().putObject("headers",
                                new JsonObject().putString("Host", domain).putString("X-Forwarded-Proto", scheme)
                                        .putString("Accept-Language", language)))
                        .putObject("parameters", parameters).putString("resourceName", notificationName)
                        .putString("template", template),
                new Handler<Message<JsonObject>>() {
                    public void handle(Message<JsonObject> event) {
                        handler.handle(event.body().getString("processedTemplate", ""));
                    }
                });
    }

    /**
     * Retrieves stored properties for a single notification.
     *
     * @param notificationName : Name of the notification
     * @param handler : Handles the properties
     */
    private void getNotificationProperties(String notificationName, final Handler<JsonObject> handler) {
        eb.send(TIMELINE_ADDRESS, new JsonObject().putString("action", "get-notification-properties")
                .putString("key", notificationName), new Handler<Message<JsonObject>>() {
                    public void handle(Message<JsonObject> event) {
                        if (!"error".equals(event.body().getString("status", "error"))) {
                            handler.handle(event.body().getObject("result"));
                        } else {
                            handler.handle(null);
                        }
                    }
                });
    }

    /**
     * Retrieves default properties for all notifications.
     * @param handler : Handles the properties
     */
    private void getNotificationsDefaults(final Handler<JsonArray> handler) {
        eb.send(TIMELINE_ADDRESS, new JsonObject().putString("action", "list-notifications-defaults"),
                new Handler<Message<JsonObject>>() {
                    public void handle(Message<JsonObject> event) {
                        if (!"error".equals(event.body().getString("status", "error"))) {
                            handler.handle(event.body().getArray("results"));
                        } else {
                            handler.handle(null);
                        }
                    }
                });
    }

    /**
     * Retrieves stored timeline user preferences.
     *
     * @param userIds : Ids of the users
     * @param handler : Handles the preferences
     */
    private void getUsersPreferences(JsonArray userIds, final Handler<JsonArray> handler) {
        eb.send(USERBOOK_ADDRESS, new JsonObject().putString("action", "get.userlist")
                .putString("application", "timeline")
                .putString("additionalMatch",
                        ", u-[:IN]->(g:Group)-[:AUTHORIZED]-(r:Role)-[:AUTHORIZE]->(act:WorkflowAction) ")
                .putString("additionalWhere",
                        "AND act.name = \"org.entcore.timeline.controllers.TimelineController|mixinConfig\" ")
                .putString("additionalCollectFields", ", language: uac.language").putArray("userIds", userIds),
                new Handler<Message<JsonObject>>() {
                    public void handle(Message<JsonObject> event) {
                        if (!"error".equals(event.body().getString("status"))) {
                            handler.handle(event.body().getArray("results"));
                        } else {
                            handler.handle(null);
                        }
                    }
                });
    }

    /**
     * Retrieves users having an email address, paginated.
     *
     * @param page : Page number
     * @param handler : Handles the users
     */
    private void getImpactedUsers(int page, final Handler<Either<String, JsonArray>> handler) {
        String query = "MATCH (u:User), u-[:IN]->(g:Group)-[:AUTHORIZED]-(r:Role)-[:AUTHORIZE]->(act:WorkflowAction) "
                + "WHERE u.activationCode IS NULL AND u.email IS NOT NULL AND length(u.email) > 0 "
                + "AND act.name = \"org.entcore.timeline.controllers.TimelineController|mixinConfig\""
                + "RETURN DISTINCT u.email as mail, u.id as id " + "SKIP {skip} LIMIT {limit}";
        JsonObject params = new JsonObject().putNumber("skip", page * USERS_LIMIT).putNumber("limit", USERS_LIMIT);
        neo4j.execute(query, params, Neo4jResult.validResultHandler(handler));
    }

    /**
     * Retrieves all timeline notifications from mongodb for a single user, from a specific date in the past.
     *
     * @param userId : Userid
     * @param from : The starting date
     * @param handler : Handles the notifications
     */
    private void getUserNotifications(String userId, Date from, final Handler<JsonArray> handler) {
        JsonObject matcher = MongoQueryBuilder.build(QueryBuilder.start("recipients")
                .elemMatch(QueryBuilder.start("userId").is(userId).get()).and("date").greaterThanEquals(from));

        mongo.find("timeline", matcher, new Handler<Message<JsonObject>>() {
            public void handle(Message<JsonObject> event) {
                if ("error".equals(event.body().getString("status", "error"))) {
                    handler.handle(new JsonArray());
                } else {
                    handler.handle(event.body().getArray("results"));
                }
            }
        });
    }

    /**
     * Retrieves an aggregated list of notifications from mongodb for a single user.
     *
     *  Notifications are grouped by type & event-type.
     * @param userId : Userid
     * @param from : Starting date in the past
     * @param handler: Handles the notifications
     */
    private void getAggregatedUserNotifications(String userId, Date from, final Handler<JsonArray> handler) {
        final JsonObject aggregation = new JsonObject();
        JsonArray pipeline = new JsonArray();
        aggregation.putString("aggregate", "timeline").putBoolean("allowDiskUse", true).putArray("pipeline",
                pipeline);

        JsonObject matcher = MongoQueryBuilder.build(QueryBuilder.start("recipients")
                .elemMatch(QueryBuilder.start("userId").is(userId).get()).and("date").greaterThanEquals(from));
        JsonObject grouper = new JsonObject(
                "{ \"_id\" : { \"type\": \"$type\", \"event-type\": \"$event-type\"}, \"count\": { \"$sum\": 1 } }");
        JsonObject transformer = new JsonObject(
                "{ \"type\": \"$_id.type\", \"event-type\": \"$_id.event-type\", \"count\": 1, \"_id\": 0 }");

        pipeline.addObject(new JsonObject().putObject("$match", matcher));
        pipeline.add(new JsonObject().putObject("$group", grouper));
        pipeline.add(new JsonObject().putObject("$project", transformer));

        mongo.command(aggregation.toString(), new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                if ("error".equals(event.body().getString("status", "error"))) {
                    handler.handle(new JsonArray());
                } else {
                    handler.handle(
                            event.body().getObject("result", new JsonObject()).getArray("result", new JsonArray()));
                }
            }

        });
    }

    /**
     * Returns either user preferences or defaults when the user has not chosen specific values.
     *
     * @param field : Which preference
     * @param userPrefs : User preferences
     * @param defaultPrefs : Default preferences
     * @return The prevailing preference
     */
    private String notificationPrefsMixin(String field, JsonObject userPrefs, JsonObject defaultPrefs) {
        return userPrefs.getString(field, defaultPrefs.getString(field, ""));
    }

    /**
     * Sends immediate notification emails for users that are concerned.
     *
     * @param request : Request initiating the notification.
     * @param notificationName : Name of the notification
     * @param notification : Notification properties
     * @param templateParameters : Notification parameters
     * @param recipientIds : Recipients of the notification
     */
    public void sendImmediateMails(final HttpServerRequest request, final String notificationName,
            final JsonObject notification, final JsonObject templateParameters, final List<String> recipientIds) {
        //Get notification properties (mixin : admin console configuration which overrides default properties)
        getNotificationProperties(notificationName, new Handler<JsonObject>() {
            public void handle(final JsonObject properties) {
                if (properties == null) {
                    log.error("[sendImmediateMails] Issue while retrieving notification (" + notificationName
                            + ") properties.");
                    return;
                }
                //Get users preferences (overrides notification properties)
                getUsersPreferences(new JsonArray(recipientIds.toArray()), new Handler<JsonArray>() {
                    public void handle(final JsonArray userList) {
                        if (userList == null) {
                            log.error("[sendImmediateMails] Issue while retrieving users preferences.");
                        }
                        //Process template once by domain
                        final Map<String, Map<String, String>> processedTemplates = new HashMap<String, Map<String, String>>();
                        templateParameters.putString("innerTemplate", notification.getString("template", ""));

                        final Map<String, Map<String, List<Object>>> toByDomainLang = new HashMap<>();

                        final AtomicInteger userCount = new AtomicInteger(userList.size());
                        final VoidHandler templatesHandler = new VoidHandler() {
                            protected void handle() {
                                if (userCount.decrementAndGet() == 0) {
                                    if (toByDomainLang.size() > 0) {
                                        //On completion : log
                                        final Handler<Message<JsonObject>> completionHandler = new Handler<Message<JsonObject>>() {
                                            public void handle(Message<JsonObject> event) {
                                                if ("error".equals(event.body().getString("status", "error"))) {
                                                    log.error(
                                                            "[Timeline immediate emails] Error while sending mails : "
                                                                    + event.body());
                                                } else {
                                                    log.debug("[Timeline immediate emails] Immediate mails sent.");
                                                }
                                            }
                                        };

                                        JsonArray keys = new JsonArray()
                                                .add("timeline.immediate.mail.subject.header")
                                                .add(notificationName.toLowerCase());

                                        for (final String domain : toByDomainLang.keySet()) {
                                            for (final String lang : toByDomainLang.get(domain).keySet()) {
                                                translateTimeline(keys, domain, lang, new Handler<JsonArray>() {
                                                    public void handle(JsonArray translations) {
                                                        //Send mail containing the "immediate" notification
                                                        emailSender.sendEmail(request,
                                                                toByDomainLang.get(domain).get(lang), null, null,
                                                                translations.get(0).toString()
                                                                        + translations.get(1).toString(),
                                                                processedTemplates.get(domain).get(lang), null,
                                                                false, completionHandler);
                                                    }
                                                });
                                            }
                                        }
                                    }
                                }
                            }
                        };

                        for (Object userObj : userList) {
                            final JsonObject userPref = ((JsonObject) userObj);
                            final String userDomain = userPref.getString("lastDomain", I18n.DEFAULT_DOMAIN);
                            final String userScheme = userPref.getString("lastScheme", "http");
                            String mutableLanguage = "fr";
                            try {
                                mutableLanguage = new JsonObject(userPref.getString("language", "{}"))
                                        .getString("default-domain", "fr");
                            } catch (Exception e) {
                                log.error("UserId [" + userPref.getString("userId", "")
                                        + "] - Bad language preferences format");
                            }
                            final String userLanguage = mutableLanguage;

                            if (!processedTemplates.containsKey(userDomain))
                                processedTemplates.put(userDomain, new HashMap<String, String>());

                            if (!processedTemplates.get(userDomain).containsKey(userLanguage)) {
                                processTimelineTemplate(templateParameters, "", "notifications/immediate-mail.html",
                                        userDomain, userScheme, userLanguage, new Handler<String>() {
                                            public void handle(String processedTemplate) {
                                                processedTemplates.get(userDomain).put(userLanguage,
                                                        processedTemplate);
                                                templatesHandler.handle(null);
                                            }
                                        });
                            } else {
                                templatesHandler.handle(null);
                            }
                            JsonObject notificationPreference = userPref.getObject("preferences", new JsonObject())
                                    .getObject("config", new JsonObject())
                                    .getObject(notificationName, new JsonObject());
                            // If the frequency is IMMEDIATE
                            // and the restriction is not INTERNAL (timeline only)
                            // and if the user has provided an email
                            if (TimelineNotificationsLoader.Frequencies.IMMEDIATE.name()
                                    .equals(notificationPreference.getString("defaultFrequency",
                                            properties.getString("defaultFrequency")))
                                    && !TimelineNotificationsLoader.Restrictions.INTERNAL.name()
                                            .equals(notificationPreference.getString("restriction",
                                                    properties.getString("restriction")))
                                    && !TimelineNotificationsLoader.Restrictions.HIDDEN.name()
                                            .equals(notificationPreference.getString("restriction",
                                                    properties.getString("restriction")))
                                    && userPref.getString("userMail") != null
                                    && !userPref.getString("userMail").trim().isEmpty()) {
                                if (!toByDomainLang.containsKey(userDomain)) {
                                    toByDomainLang.put(userDomain, new HashMap<String, List<Object>>());
                                }
                                if (!toByDomainLang.get(userDomain).containsKey(userLanguage)) {
                                    toByDomainLang.get(userDomain).put(userLanguage, new ArrayList<Object>());
                                }
                                toByDomainLang.get(userDomain).get(userLanguage)
                                        .add(userPref.getString("userMail"));
                            }
                        }
                    }
                });
            }
        });
    }

    /**
     * Send daily notification emails for all users.
     *
     * @param dayDelta : When to aggregate, delta from now.
     * @param handler : Handles the results, emails sent / users KO
     */
    public void sendDailyMails(int dayDelta, final Handler<Either<String, JsonObject>> handler) {

        final HttpServerRequest request = new JsonHttpServerRequest(new JsonObject());
        final AtomicInteger userPagination = new AtomicInteger(0);
        final Calendar dayDate = Calendar.getInstance();
        dayDate.add(Calendar.DAY_OF_MONTH, dayDelta);
        dayDate.set(Calendar.HOUR_OF_DAY, 0);
        dayDate.set(Calendar.MINUTE, 0);
        dayDate.set(Calendar.SECOND, 0);
        dayDate.set(Calendar.MILLISECOND, 0);

        final JsonObject results = new JsonObject().putNumber("mails.sent", 0).putNumber("users.ko", 0);
        final JsonObject notificationsDefaults = new JsonObject();

        final Handler<Boolean> userContinuationHandler = new Handler<Boolean>() {

            private final Handler<Boolean> continuation = this;
            private final Handler<JsonArray> usersHandler = new Handler<JsonArray>() {
                public void handle(final JsonArray users) {
                    final int nbUsers = users.size();
                    if (nbUsers == 0) {
                        continuation.handle(false);
                        return;
                    }
                    final AtomicInteger usersCountdown = new AtomicInteger(nbUsers);

                    final VoidHandler usersEndHandler = new VoidHandler() {
                        protected void handle() {
                            if (usersCountdown.decrementAndGet() <= 0) {
                                continuation.handle(nbUsers == USERS_LIMIT);
                            }
                        }
                    };

                    final JsonArray userIds = new JsonArray();
                    for (Object userObj : users)
                        userIds.add(((JsonObject) userObj).getString("id", ""));
                    getUsersPreferences(userIds, new Handler<JsonArray>() {
                        public void handle(JsonArray preferences) {
                            for (Object userObj : preferences) {
                                final JsonObject userPrefs = (JsonObject) userObj;
                                final String userDomain = userPrefs.getString("lastDomain", I18n.DEFAULT_DOMAIN);
                                final String userScheme = userPrefs.getString("lastScheme", "http");
                                String mutableUserLanguage = "fr";
                                try {
                                    mutableUserLanguage = new JsonObject(userPrefs.getString("language", "{}"))
                                            .getString("default-domain", "fr");
                                } catch (Exception e) {
                                    log.error("UserId [" + userPrefs.getString("userId", "")
                                            + "] - Bad language preferences format");
                                }
                                final String userLanguage = mutableUserLanguage;

                                getUserNotifications(userPrefs.getString("userId", ""), dayDate.getTime(),
                                        new Handler<JsonArray>() {
                                            public void handle(JsonArray notifications) {
                                                if (notifications.size() == 0) {
                                                    usersEndHandler.handle(null);
                                                    return;
                                                }

                                                SimpleDateFormat formatter = new SimpleDateFormat(
                                                        "EEE, d MMM yyyy HH:mm:ss",
                                                        Locale.forLanguageTag(userLanguage));
                                                final JsonArray dates = new JsonArray();
                                                final JsonArray templates = new JsonArray();

                                                for (Object notificationObj : notifications) {
                                                    JsonObject notification = (JsonObject) notificationObj;
                                                    final String notificationName = notification
                                                            .getString("type", "").toLowerCase() + "."
                                                            + notification.getString("event-type", "")
                                                                    .toLowerCase();
                                                    if (notificationsDefaults.getObject(notificationName) == null)
                                                        continue;

                                                    JsonObject notificationPreference = userPrefs
                                                            .getObject("preferences", new JsonObject())
                                                            .getObject("config", new JsonObject())
                                                            .getObject(notificationName, new JsonObject());
                                                    if (TimelineNotificationsLoader.Frequencies.DAILY.name()
                                                            .equals(notificationPrefsMixin("defaultFrequency",
                                                                    notificationPreference,
                                                                    notificationsDefaults
                                                                            .getObject(notificationName)))
                                                            && !TimelineNotificationsLoader.Restrictions.INTERNAL
                                                                    .name()
                                                                    .equals(notificationPrefsMixin("restriction",
                                                                            notificationPreference,
                                                                            notificationsDefaults
                                                                                    .getObject(notificationName)))
                                                            && !TimelineNotificationsLoader.Restrictions.HIDDEN
                                                                    .name()
                                                                    .equals(notificationPrefsMixin("restriction",
                                                                            notificationPreference,
                                                                            notificationsDefaults.getObject(
                                                                                    notificationName)))) {
                                                        templates.add(new JsonObject()
                                                                .putString("template",
                                                                        notificationsDefaults
                                                                                .getObject(notificationName,
                                                                                        new JsonObject())
                                                                                .getString("template", ""))
                                                                .putObject("params", notification
                                                                        .getObject("params", new JsonObject())));
                                                        dates.add(formatter.format(MongoDb
                                                                .parseIsoDate(notification.getObject("date"))));
                                                    }
                                                }
                                                if (templates.size() > 0) {
                                                    JsonObject templateParams = new JsonObject()
                                                            .putArray("nestedTemplatesArray", templates)
                                                            .putArray("notificationDates", dates);
                                                    processTimelineTemplate(templateParams, "",
                                                            "notifications/daily-mail.html", userDomain, userScheme,
                                                            userLanguage, new Handler<String>() {
                                                                public void handle(final String processedTemplate) {
                                                                    //On completion : log
                                                                    final Handler<Message<JsonObject>> completionHandler = new Handler<Message<JsonObject>>() {
                                                                        public void handle(
                                                                                Message<JsonObject> event) {
                                                                            if ("error".equals(
                                                                                    event.body().getString("status",
                                                                                            "error"))) {
                                                                                log.error(
                                                                                        "[Timeline daily emails] Error while sending mail : "
                                                                                                + event.body());
                                                                                results.putNumber("users.ko",
                                                                                        results.getInteger(
                                                                                                "users.ko") + 1);
                                                                            } else {
                                                                                results.putNumber("mails.sent",
                                                                                        results.getInteger(
                                                                                                "mails.sent") + 1);
                                                                            }
                                                                            usersEndHandler.handle(null);
                                                                        }

                                                                    };

                                                                    //Translate mail title
                                                                    JsonArray keys = new JsonArray().add(
                                                                            "timeline.daily.mail.subject.header");
                                                                    translateTimeline(keys, userDomain,
                                                                            userLanguage, new Handler<JsonArray>() {
                                                                                public void handle(
                                                                                        JsonArray translations) {
                                                                                    //Send mail containing the "daily" notifications
                                                                                    emailSender.sendEmail(request,
                                                                                            userPrefs.getString(
                                                                                                    "userMail", ""),
                                                                                            null, null,
                                                                                            translations.get(0)
                                                                                                    .toString(),
                                                                                            processedTemplate, null,
                                                                                            false,
                                                                                            completionHandler);
                                                                                }
                                                                            });
                                                                }
                                                            });
                                                } else {
                                                    usersEndHandler.handle(null);
                                                }

                                            }
                                        });
                            }
                        }
                    });
                }
            };

            public void handle(Boolean continuation) {
                if (continuation) {
                    getImpactedUsers(userPagination.getAndIncrement(), new Handler<Either<String, JsonArray>>() {
                        public void handle(Either<String, JsonArray> event) {
                            if (event.isLeft()) {
                                log.error("[sendDailyMails] Error while retrieving impacted users : "
                                        + event.left().getValue());
                                handler.handle(new Either.Left<String, JsonObject>(event.left().getValue()));
                            } else {
                                JsonArray users = event.right().getValue();
                                usersHandler.handle(users);
                            }
                        }
                    });
                } else {
                    handler.handle(new Either.Right<String, JsonObject>(results));
                }
            }
        };

        getNotificationsDefaults(new Handler<JsonArray>() {
            public void handle(final JsonArray notifications) {
                if (notifications == null) {
                    log.error("[sendDailyMails] Error while retrieving notifications defaults.");
                    return;
                } else {
                    for (Object notifObj : notifications) {
                        final JsonObject notif = (JsonObject) notifObj;
                        notificationsDefaults.putObject(notif.getString("key", ""), notif);
                    }
                    userContinuationHandler.handle(true);
                }
            }
        });
    }

    /**
     * Sends weekly notification emails for all users.
     *
     * @param dayDelta : Day from which to aggregate, delta from now.
     * @param handler : Handles the results, emails sent / users KO
     */
    public void sendWeeklyMails(int dayDelta, final Handler<Either<String, JsonObject>> handler) {

        final HttpServerRequest request = new JsonHttpServerRequest(new JsonObject());
        final AtomicInteger userPagination = new AtomicInteger(0);
        final Calendar weekDate = Calendar.getInstance();
        weekDate.add(Calendar.DAY_OF_MONTH, dayDelta - 6);
        weekDate.set(Calendar.HOUR_OF_DAY, 0);
        weekDate.set(Calendar.MINUTE, 0);
        weekDate.set(Calendar.SECOND, 0);
        weekDate.set(Calendar.MILLISECOND, 0);

        final JsonObject results = new JsonObject().putNumber("mails.sent", 0).putNumber("users.ko", 0);
        final JsonObject notificationsDefaults = new JsonObject();

        final Handler<Boolean> userContinuationHandler = new Handler<Boolean>() {

            private final Handler<Boolean> continuation = this;
            private final Handler<JsonArray> usersHandler = new Handler<JsonArray>() {
                public void handle(final JsonArray users) {
                    final int nbUsers = users.size();
                    if (nbUsers == 0) {
                        continuation.handle(false);
                        return;
                    }
                    final AtomicInteger usersCountdown = new AtomicInteger(nbUsers);

                    final VoidHandler usersEndHandler = new VoidHandler() {
                        protected void handle() {
                            if (usersCountdown.decrementAndGet() <= 0) {
                                continuation.handle(nbUsers == USERS_LIMIT);
                            }
                        }
                    };

                    final JsonArray userIds = new JsonArray();
                    for (Object userObj : users)
                        userIds.add(((JsonObject) userObj).getString("id", ""));
                    getUsersPreferences(userIds, new Handler<JsonArray>() {
                        public void handle(JsonArray preferences) {
                            for (Object userObj : preferences) {
                                final JsonObject userPrefs = (JsonObject) userObj;
                                final String userDomain = userPrefs.getString("lastDomain", I18n.DEFAULT_DOMAIN);
                                final String userScheme = userPrefs.getString("lastScheme", "http");
                                String mutableUserLanguage = "fr";
                                try {
                                    mutableUserLanguage = new JsonObject(userPrefs.getString("language", "{}"))
                                            .getString("default-domain", "fr");
                                } catch (Exception e) {
                                    log.error("UserId [" + userPrefs.getString("userId", "")
                                            + "] - Bad language preferences format");
                                }
                                final String userLanguage = mutableUserLanguage;

                                getAggregatedUserNotifications(userPrefs.getString("userId", ""),
                                        weekDate.getTime(), new Handler<JsonArray>() {
                                            public void handle(JsonArray notifications) {
                                                if (notifications.size() == 0) {
                                                    usersEndHandler.handle(null);
                                                    return;
                                                }

                                                final JsonArray weeklyNotifications = new JsonArray();

                                                for (Object notificationObj : notifications) {
                                                    JsonObject notification = (JsonObject) notificationObj;
                                                    final String notificationName = notification
                                                            .getString("type", "").toLowerCase() + "."
                                                            + notification.getString("event-type", "")
                                                                    .toLowerCase();
                                                    if (notificationsDefaults.getObject(notificationName) == null)
                                                        continue;

                                                    JsonObject notificationPreference = userPrefs
                                                            .getObject("preferences", new JsonObject())
                                                            .getObject("config", new JsonObject())
                                                            .getObject(notificationName, new JsonObject());
                                                    if (TimelineNotificationsLoader.Frequencies.WEEKLY.name()
                                                            .equals(notificationPrefsMixin("defaultFrequency",
                                                                    notificationPreference,
                                                                    notificationsDefaults
                                                                            .getObject(notificationName)))
                                                            && !TimelineNotificationsLoader.Restrictions.INTERNAL
                                                                    .name()
                                                                    .equals(notificationPrefsMixin("restriction",
                                                                            notificationPreference,
                                                                            notificationsDefaults
                                                                                    .getObject(notificationName)))
                                                            && !TimelineNotificationsLoader.Restrictions.HIDDEN
                                                                    .name()
                                                                    .equals(notificationPrefsMixin("restriction",
                                                                            notificationPreference,
                                                                            notificationsDefaults.getObject(
                                                                                    notificationName)))) {
                                                        notification.putString("notificationName",
                                                                notificationName);
                                                        weeklyNotifications.add(notification);
                                                    }
                                                }

                                                final JsonObject weeklyNotificationsObj = new JsonObject();
                                                final JsonArray weeklyNotificationsGroupedArray = new JsonArray();
                                                for (Object notif : weeklyNotifications) {
                                                    JsonObject notification = (JsonObject) notif;
                                                    if (!weeklyNotificationsObj.containsField(
                                                            notification.getString("type").toLowerCase()))
                                                        weeklyNotificationsObj.putObject(
                                                                notification.getString("type").toLowerCase(),
                                                                new JsonObject()
                                                                        .putString("link", notificationsDefaults
                                                                                .getObject(notification.getString(
                                                                                        "notificationName"))
                                                                                .getString("app-address", ""))
                                                                        .putArray("event-types", new JsonArray()));
                                                    weeklyNotificationsObj
                                                            .getObject(notification.getString("type").toLowerCase())
                                                            .getArray(("event-types"), new JsonArray())
                                                            .add(notification);
                                                }

                                                for (String key : weeklyNotificationsObj.toMap().keySet()) {
                                                    weeklyNotificationsGroupedArray.add(new JsonObject()
                                                            .putString("type", key)
                                                            .putString("link",
                                                                    weeklyNotificationsObj.getObject(key)
                                                                            .getString("link", ""))
                                                            .putArray("event-types", weeklyNotificationsObj
                                                                    .getObject(key).getArray("event-types")));
                                                }

                                                if (weeklyNotifications.size() > 0) {
                                                    JsonObject templateParams = new JsonObject().putArray(
                                                            "notifications", weeklyNotificationsGroupedArray);
                                                    processTimelineTemplate(templateParams, "",
                                                            "notifications/weekly-mail.html", userDomain,
                                                            userScheme, userLanguage, new Handler<String>() {
                                                                public void handle(final String processedTemplate) {
                                                                    //On completion : log
                                                                    final Handler<Message<JsonObject>> completionHandler = new Handler<Message<JsonObject>>() {
                                                                        public void handle(
                                                                                Message<JsonObject> event) {
                                                                            if ("error".equals(
                                                                                    event.body().getString("status",
                                                                                            "error"))) {
                                                                                log.error(
                                                                                        "[Timeline weekly emails] Error while sending mail : "
                                                                                                + event.body());
                                                                                results.putNumber("users.ko",
                                                                                        results.getInteger(
                                                                                                "users.ko") + 1);
                                                                            } else {
                                                                                results.putNumber("mails.sent",
                                                                                        results.getInteger(
                                                                                                "mails.sent") + 1);
                                                                            }
                                                                            usersEndHandler.handle(null);
                                                                        }

                                                                    };

                                                                    //Translate mail title
                                                                    JsonArray keys = new JsonArray().add(
                                                                            "timeline.weekly.mail.subject.header");
                                                                    translateTimeline(keys, userDomain,
                                                                            userLanguage, new Handler<JsonArray>() {
                                                                                public void handle(
                                                                                        JsonArray translations) {
                                                                                    //Send mail containing the "weekly" notifications
                                                                                    emailSender.sendEmail(request,
                                                                                            userPrefs.getString(
                                                                                                    "userMail", ""),
                                                                                            null, null,
                                                                                            translations.get(0)
                                                                                                    .toString(),
                                                                                            processedTemplate, null,
                                                                                            false,
                                                                                            completionHandler);
                                                                                }
                                                                            });
                                                                }
                                                            });
                                                } else {
                                                    usersEndHandler.handle(null);
                                                }

                                            }
                                        });
                            }
                        }
                    });
                }
            };

            public void handle(Boolean continuation) {
                if (continuation) {
                    getImpactedUsers(userPagination.getAndIncrement(), new Handler<Either<String, JsonArray>>() {
                        public void handle(Either<String, JsonArray> event) {
                            if (event.isLeft()) {
                                log.error("[sendWeeklyMails] Error while retrieving impacted users : "
                                        + event.left().getValue());
                                handler.handle(new Either.Left<String, JsonObject>(event.left().getValue()));
                            } else {
                                JsonArray users = event.right().getValue();
                                usersHandler.handle(users);
                            }
                        }
                    });
                } else {
                    handler.handle(new Either.Right<String, JsonObject>(results));
                }
            }
        };

        getNotificationsDefaults(new Handler<JsonArray>() {
            public void handle(final JsonArray notifications) {
                if (notifications == null) {
                    log.error("[sendWeeklyMails] Error while retrieving notifications defaults.");
                    return;
                } else {
                    for (Object notifObj : notifications) {
                        final JsonObject notif = (JsonObject) notifObj;
                        notificationsDefaults.putObject(notif.getString("key", ""), notif);
                    }
                    userContinuationHandler.handle(true);
                }
            }
        });
    }

}