org.exoplatform.outlook.OutlookServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.exoplatform.outlook.OutlookServiceImpl.java

Source

/*
 * Copyright (C) 2003-2016 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.outlook;

import com.ibm.icu.text.Transliterator;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.http.entity.ContentType;
import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.configuration.ConfigurationException;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.forum.bbcode.core.ExtendedBBCodeProvider;
import org.exoplatform.forum.common.CommonUtils;
import org.exoplatform.forum.common.TransformHTML;
import org.exoplatform.forum.common.webui.WebUIUtils;
import org.exoplatform.forum.ext.activity.BuildLinkUtils;
import org.exoplatform.forum.ext.activity.BuildLinkUtils.PORTLET_INFO;
import org.exoplatform.forum.service.Category;
import org.exoplatform.forum.service.Forum;
import org.exoplatform.forum.service.ForumAdministration;
import org.exoplatform.forum.service.ForumService;
import org.exoplatform.forum.service.ForumServiceUtils;
import org.exoplatform.forum.service.MessageBuilder;
import org.exoplatform.forum.service.Topic;
import org.exoplatform.outlook.forum.ForumUtils;
import org.exoplatform.outlook.jcr.File;
import org.exoplatform.outlook.jcr.Folder;
import org.exoplatform.outlook.jcr.HierarchyNode;
import org.exoplatform.outlook.jcr.NodeFinder;
import org.exoplatform.outlook.jcr.UserDocuments;
import org.exoplatform.outlook.mail.MailAPI;
import org.exoplatform.outlook.mail.MailServerException;
import org.exoplatform.outlook.social.OutlookAttachmentActivity;
import org.exoplatform.outlook.social.OutlookMessageActivity;
import org.exoplatform.portal.application.PortalRequestContext;
import org.exoplatform.portal.config.model.PortalConfig;
import org.exoplatform.portal.mop.SiteType;
import org.exoplatform.portal.webui.util.Util;
import org.exoplatform.services.cms.BasePath;
import org.exoplatform.services.cms.documents.TrashService;
import org.exoplatform.services.cms.drives.DriveData;
import org.exoplatform.services.cms.drives.ManageDriveService;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.access.AccessControlList;
import org.exoplatform.services.jcr.access.PermissionType;
import org.exoplatform.services.jcr.core.ExtendedNode;
import org.exoplatform.services.jcr.ext.app.SessionProviderService;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.listener.ListenerService;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.Group;
import org.exoplatform.services.organization.Membership;
import org.exoplatform.services.organization.MembershipType;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.resources.ResourceBundleService;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.security.IdentityConstants;
import org.exoplatform.services.wcm.core.NodetypeConstant;
import org.exoplatform.social.core.activity.model.ExoSocialActivity;
import org.exoplatform.social.core.activity.model.ExoSocialActivityImpl;
import org.exoplatform.social.core.application.PeopleService;
import org.exoplatform.social.core.application.SpaceActivityPublisher;
import org.exoplatform.social.core.identity.model.Identity;
import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider;
import org.exoplatform.social.core.manager.ActivityManager;
import org.exoplatform.social.core.manager.IdentityManager;
import org.exoplatform.social.core.service.LinkProvider;
import org.exoplatform.social.core.space.SpaceUtils;
import org.exoplatform.social.core.space.model.Space;
import org.exoplatform.social.core.space.spi.SpaceService;
import org.exoplatform.social.webui.activity.UIDefaultActivity;
import org.exoplatform.wcm.webui.reader.ContentReader;
import org.exoplatform.web.url.navigation.NavigationResource;
import org.exoplatform.web.url.navigation.NodeURL;
import org.exoplatform.webui.application.WebuiRequestContext;
import org.exoplatform.wiki.WikiException;
import org.exoplatform.wiki.mow.api.Page;
import org.exoplatform.wiki.mow.api.Permission;
import org.exoplatform.wiki.mow.api.PermissionEntry;
import org.exoplatform.wiki.mow.api.Wiki;
import org.exoplatform.wiki.rendering.RenderingService;
import org.exoplatform.wiki.resolver.TitleResolver;
import org.exoplatform.wiki.service.IDType;
import org.exoplatform.wiki.service.WikiService;
import org.exoplatform.ws.frameworks.json.value.JsonValue;
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
import org.picocontainer.Startable;
import org.xwiki.rendering.syntax.Syntax;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.AccessControlException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.servlet.http.HttpServletRequest;

/**
 * Service implementing {@link OutlookService} and {@link Startable}.<br>
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:pnedonosko@exoplatform.com">Peter Nedonosko</a>
 * @version $Id: AnaplanServiceImpl.java 00000 Mar 3, 2016 pnedonosko $
 */
public class OutlookServiceImpl implements OutlookService, Startable {

    /** The Constant MAILSERVER_URL. */
    public static final String MAILSERVER_URL = "mailserver-url";

    /** The Constant EXO_PRIVILEGEABLE. */
    protected static final String EXO_PRIVILEGEABLE = "exo:privilegeable";

    /** The Constant OUTLOOK_MESSAGES_TITLE. */
    protected static final String OUTLOOK_MESSAGES_TITLE = "Outlook Messages";

    /** The Constant OUTLOOK_MESSAGES_NAME. */
    protected static final String OUTLOOK_MESSAGES_NAME = "outlook-messages";

    /** The Constant WIKI_PERMISSION_ANY. */
    protected static final String WIKI_PERMISSION_ANY = "any";

    /** The Constant UPLAODS_FOLDER_TITLE. */
    protected static final String UPLAODS_FOLDER_TITLE = "Uploads";

    /** The Constant SPACES_HOME. */
    protected static final String SPACES_HOME = "/Groups/spaces";

    /** The Constant ROOT_USER. */
    protected static final String ROOT_USER = "root";

    /** The Constant PERSONAL_DOCUMENTS. */
    protected static final String PERSONAL_DOCUMENTS = "Personal Documents";

    /** The Constant READER_PERMISSION. */
    protected static final String[] READER_PERMISSION = new String[] { PermissionType.READ };

    /** The Constant MANAGER_PERMISSION. */
    protected static final String[] MANAGER_PERMISSION = new String[] { PermissionType.READ,
            PermissionType.REMOVE };

    /** The Constant LOG. */
    protected static final Log LOG = ExoLogger.getLogger(OutlookServiceImpl.class);

    /** The Constant RANDOM. */
    protected static final Random RANDOM = new Random();

    /** The Constant accentsConverter. */
    protected static final Transliterator accentsConverter = Transliterator
            .getInstance("Latin; NFD; [:Nonspacing Mark:] Remove; NFC;");

    /**
     * The Class UserFolder.
     */
    protected class UserFolder extends Folder {

        /**
         * Instantiates a new user folder.
         *
         * @param parent the parent
         * @param node the node
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected UserFolder(Folder parent, Node node) throws RepositoryException, OutlookException {
            super(parent, node);
        }

        /**
         * Instantiates a new user folder.
         *
         * @param parentPath the parent path
         * @param node the node
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected UserFolder(String parentPath, Node node) throws RepositoryException, OutlookException {
            super(parentPath, node);
        }

        /**
         * Instantiates a new user folder.
         *
         * @param node the node
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected UserFolder(Node node) throws RepositoryException, OutlookException {
            super(node.getPath(), node);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Folder addSubfolder(String name) throws RepositoryException, OutlookException {
            final Node parent = getNode();
            Node subfolderNode = addFolder(parent, name, true);
            Folder subfolder = newFolder(this, subfolderNode);
            parent.save();
            Set<Folder> subfolders = this.subfolders.get();
            if (subfolders != null) {
                subfolders.add(subfolder);
            }
            return subfolder;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected Locale userLocale() {
            return currentUserLocale();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected Folder newFolder(Folder parent, Node node) throws RepositoryException, OutlookException {
            Folder folder = new UserFolder(parent, node);
            return folder;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected Folder newFolder(String rootPath, Node node) throws RepositoryException, OutlookException {
            Folder folder = new UserFolder(rootPath, node);
            return folder;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected File newFile(Folder parent, Node node) throws RepositoryException, OutlookException {
            File file = new UserFile(parent, node);
            return file;
        }

    }

    /**
     * The Class PersonalDocumentsFolder.
     */
    protected class PersonalDocumentsFolder extends UserFolder implements UserDocuments {

        /**
         * Instantiates a new personal documents folder.
         *
         * @param node the node
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected PersonalDocumentsFolder(Node node) throws RepositoryException, OutlookException {
            super(node);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Folder getRootFolder() throws OutlookException {
            return this;
        }

        /**
         * {@inheritDoc}
         * 
         */
        @Override
        public Folder getFolder(String path) throws OutlookException, RepositoryException {
            String rootPath = getPath();
            Folder folder;
            String folderPath = HierarchyNode.getPath(path);
            if (rootPath.equals(folderPath)) {
                folder = this;
            } else if (folderPath.startsWith(rootPath)) {
                Node node = node(folderPath);
                folder = new UserFolder(node.getParent().getPath(), node);
            } else {
                throw new BadParameterException("Path does not belong to space documents: " + path);
            }
            initDocumentLink(this, folder);
            return folder;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<File> findAllLastDocuments(String text) throws RepositoryException, OutlookException {
            QueryManager qm = getNode().getSession().getWorkspace().getQueryManager();

            Set<File> res = new LinkedHashSet<File>();

            if (text == null || text.length() == 0) {
                Query q = qm.createQuery("SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId()
                        + "' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL);
                final String currentUser = currentUserId();
                // fetch and filter nodes: not trashed and not system
                fetchQuery(q.execute(), 20, res, node -> {
                    try {
                        // XXX skip some known system locations/names
                        String path = node.getPath();
                        if (path.indexOf("/ApplicationData") >= 0 || path.indexOf("exo:applications") >= 0) {
                            return false;
                        }
                        // skip trashed
                        if (org.exoplatform.ecm.webui.utils.Utils.isInTrash(node)) {
                            return false;
                        }
                        // access all nodes except of owned by root, but only if it is not his session
                        AccessControlList acl = ((ExtendedNode) node).getACL();
                        String owner = acl.getOwner();
                        if (currentUser.equals(owner)) {
                            return true;
                        } else {
                            return !ROOT_USER.equals(owner);
                        }
                    } catch (RepositoryException e) {
                        // ignore it
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Error getting ACL/owner of " + node, e);
                        }
                        return false;
                    }
                });
            } else {
                Query qOwn = qm.createQuery("SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId()
                        + "' AND jcr:path LIKE '" + getPath() + "/%' AND exo:title LIKE '%" + text
                        + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL);
                // fetch first three modified by this user only
                fetchQuery(qOwn.execute(), 3, res);
                // and add all others up to total 20 files
                Query qOthers = qm.createQuery(
                        "SELECT * FROM nt:file WHERE jcr:path LIKE '" + getPath() + "/%' AND exo:title LIKE '%"
                                + text + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC",
                        Query.SQL);
                fetchQuery(qOthers.execute(), 20 - res.size(), res);
            }

            if (res.size() < 20) {
                // If have some space, then fetch latest from spaces
                StringBuilder sql = new StringBuilder();
                sql.append("SELECT * FROM nt:file WHERE jcr:path LIKE '");
                sql.append(SPACES_HOME);
                sql.append("/%/Documents/%'");
                if (text != null && text.length() > 0) {
                    sql.append(" AND exo:title LIKE '%");
                    sql.append(text);
                    sql.append("%'");
                }
                sql.append(" ORDER BY exo:lastModifiedDate DESC, exo:title ASC");
                Query qSpaces = qm.createQuery(sql.toString(), Query.SQL);
                fetchQuery(qSpaces.execute(), 20 - res.size(), res);
            }
            return res;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<File> findLastDocuments(String text) throws RepositoryException, OutlookException {
            QueryManager qm = getNode().getSession().getWorkspace().getQueryManager();

            Set<File> res = new LinkedHashSet<File>();

            // TODO this search will not include files from user's Public folder
            Query q;
            if (text == null || text.length() == 0) {
                q = qm.createQuery("SELECT * FROM nt:file WHERE jcr:path LIKE '" + getPath()
                        + "/%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL);
            } else {
                q = qm.createQuery("SELECT * FROM nt:file WHERE jcr:path LIKE '" + getPath()
                        + "/%' AND exo:title LIKE '%" + text + "%'", Query.SQL);
            }
            fetchQuery(q.execute(), 20, res);
            return res;
        }

        /**
         * Gets the drive name.
         *
         * @return the drive name
         */
        protected String getDriveName() {
            // XXX we use what pointed in XML config
            return PERSONAL_DOCUMENTS;
        }
    }

    /**
     * The Class UserFile.
     */
    protected class UserFile extends File {

        /**
         * Instantiates a new user file.
         *
         * @param parent the parent
         * @param node the node
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected UserFile(Folder parent, Node node) throws RepositoryException, OutlookException {
            super(parent, node);
        }

        /**
         * Instantiates a new user file.
         *
         * @param parentPath the parent path
         * @param node the node
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected UserFile(String parentPath, Node node) throws RepositoryException, OutlookException {
            super(parentPath, node);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected Locale userLocale() {
            return currentUserLocale();
        }
    }

    /**
     * The Class UserImpl.
     */
    protected class UserImpl extends OutlookUser {

        /** The social identity manager. */
        protected final IdentityManager socialIdentityManager;

        /** The social activity manager. */
        protected final ActivityManager socialActivityManager;

        /**
         * Instantiates a new user impl.
         *
         * @param email the email
         * @param displayName the display name
         * @param userName the user name
         */
        protected UserImpl(String email, String displayName, String userName) {
            super(email, displayName, userName);
            this.socialIdentityManager = socialIdentityManager();
            this.socialActivityManager = socialActivityManager();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ExoSocialActivity postActivity(OutlookMessage message) throws OutlookException {
            // save text to user documents
            try {
                Node userDocs = userDocumentsNode(localUser);
                if (userDocs != null) {
                    Node userPublicFolder = userDocs.getParent().getNode("Public");
                    Node messagesFolder = messagesFolder(userPublicFolder, localUser, "member:/platform/users");
                    Node messageFile = addMessageFile(messagesFolder, message);
                    setPermissions(messageFile, localUser, "member:/platform/users");
                    messagesFolder.save();
                    message.setFileNode(messageFile);

                    String userMessage = message.getTitle();
                    userMessage = userMessage != null && userMessage.length() > 0 ? safeText(userMessage) : null;

                    final String origType = org.exoplatform.wcm.ext.component.activity.listener.Utils
                            .getActivityType();
                    try {
                        org.exoplatform.wcm.ext.component.activity.listener.Utils
                                .setActivityType(OutlookMessageActivity.ACTIVITY_TYPE);
                        ExoSocialActivity activity = org.exoplatform.wcm.ext.component.activity.listener.Utils
                                .postFileActivity(messageFile, userMessage, true, false, "", "");
                        // TODO should we care about activity removal with the message file?
                        activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId()));
                        return activity;
                    } finally {
                        org.exoplatform.wcm.ext.component.activity.listener.Utils.setActivityType(origType);
                    }
                } else {
                    throw new OutlookException("Has no Personal Documents folder for user " + localUser);
                }
            } catch (Exception e) {
                throw new OutlookException("Error posting activity for user " + localUser, e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ExoSocialActivity postActivity(String title, String body) throws Exception {
            // post activity to user status stream
            Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME,
                    currentUserId(), true);
            String safeTitle = safeText(title);
            String safeBody = safeHtml(body);
            ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(), null, safeTitle, safeBody);
            socialActivityManager.saveActivityNoReturn(userIdentity, activity);
            activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId()));
            return activity;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ExoSocialActivity postActivity(String text) throws Exception {
            // post activity to user status stream
            Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME,
                    currentUserId(), true);
            String safeText = safeActivityMessage(text);
            ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(),
                    PeopleService.PEOPLE_APP_ID, safeText, null);
            // we do like done UIDefaultActivityComposer
            activity.setType(UIDefaultActivity.ACTIVITY_TYPE);

            socialActivityManager.saveActivityNoReturn(userIdentity, activity);
            activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId()));
            return activity;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Page addWikiPage(OutlookMessage message) throws Exception {
            String wikiType = PortalConfig.PORTAL_TYPE;
            String creator = message.getUser().getLocalUser();
            List<String> users = new ArrayList<String>();
            users.add(creator);
            // TODO add space group to users?
            return createWikiPage(wikiType, "intranet", creator, message.getSubject(), messageSummary(message),
                    message.getBody(), users);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Topic addForumTopic(String categoryId, String forumId, OutlookMessage message) throws Exception {
            return createForumTopic(categoryId, forumId, message.getUser().getLocalUser(), message.getSubject(),
                    messageSummary(message), message.getBody());
        }
    }

    /**
     * The Class OutlookSpaceImpl.
     */
    protected class OutlookSpaceImpl extends OutlookSpace {

        /**
         * The Class SpaceFolder.
         */
        class SpaceFolder extends UserFolder {

            /**
             * Instantiates a new space folder.
             *
             * @param parent the parent
             * @param node the node
             * @throws RepositoryException the repository exception
             * @throws OutlookException the outlook exception
             */
            protected SpaceFolder(Folder parent, Node node) throws RepositoryException, OutlookException {
                super(parent, node);
            }

            /**
             * Instantiates a new space folder.
             *
             * @param rootPath the root path
             * @param node the node
             * @throws RepositoryException the repository exception
             * @throws OutlookException the outlook exception
             */
            protected SpaceFolder(String rootPath, Node node) throws RepositoryException, OutlookException {
                super(rootPath, node);
            }

            /**
             * {@inheritDoc}
             */
            @Override
            protected void readChildNodes() throws RepositoryException, OutlookException {
                super.readChildNodes();
                for (Folder sf : this.subfolders.get()) {
                    initDocumentLink(OutlookSpaceImpl.this, sf);
                }
                for (File f : this.files.get()) {
                    initDocumentLink(OutlookSpaceImpl.this, f);
                }
            }
        }

        /**
         * The Class RootFolder.
         */
        class RootFolder extends SpaceFolder {

            /**
             * Instantiates a new root folder.
             *
             * @param rootPath the root path
             * @param node the node
             * @throws RepositoryException the repository exception
             * @throws OutlookException the outlook exception
             */
            protected RootFolder(String rootPath, Node node) throws RepositoryException, OutlookException {
                super(rootPath, node);
                initDocumentLink(OutlookSpaceImpl.this, this);
                hasSubfolders(); // force child reading to init default folder in readSubnodes()
            }

            /**
             * {@inheritDoc}
             */
            @Override
            protected Set<Folder> readSubnodes() throws RepositoryException, OutlookException {
                Folder uploads = null;
                Set<Folder> subfolders = super.readSubnodes();
                for (Folder sf : subfolders) {
                    if (sf.getTitle().equals(UPLAODS_FOLDER_TITLE)) {
                        uploads = defaultSubfolder = sf;
                        break;
                    }
                }
                if (uploads == null) {
                    try {
                        Node subfolderNode = addFolder(node, UPLAODS_FOLDER_TITLE, false);
                        uploads = newFolder(this, subfolderNode);
                        node.save();
                        initDocumentLink(OutlookSpaceImpl.this, uploads);
                        subfolders.add(uploads);
                        defaultSubfolder = uploads;
                    } catch (AccessDeniedException e) {
                        if (LOG.isDebugEnabled()) {
                            // gather some info about the user for the log
                            String currentUserId = currentUserId();
                            StringBuilder userInfo = new StringBuilder();
                            userInfo.append(currentUserId);
                            try {
                                userInfo.append('[');
                                for (Membership m : organization.getMembershipHandler()
                                        .findMembershipsByUser(currentUserId())) {
                                    userInfo.append(m.getMembershipType());
                                    userInfo.append(':');
                                    userInfo.append(m.getGroupId());
                                    userInfo.append(' ');
                                }
                                userInfo.setCharAt(userInfo.length() - 1, ']');
                            } catch (Exception oe) {
                                LOG.warn("Error getting organization user " + currentUserId, e);
                            }
                            LOG.debug("Error creating " + UPLAODS_FOLDER_TITLE + " folder in " + getPath()
                                    + ". User: " + userInfo.toString() + ". Parent node: " + node, e);
                            // TODO we don't want throw Access error here, it should be thrown where actually will affect an
                            // user
                            // throw new AccessException("Access denied to " + OutlookSpaceImpl.this.getTitle(), e);
                        }
                        defaultSubfolder = null;
                    }
                }
                return subfolders;
            }
        }

        /** The root path. */
        protected final String rootPath;

        /** The root folder. */
        protected final ThreadLocal<RootFolder> rootFolder = new ThreadLocal<RootFolder>();

        /** The social identity manager. */
        protected final IdentityManager socialIdentityManager;

        /** The social activity manager. */
        protected final ActivityManager socialActivityManager;

        /**
         * Instantiates a new outlook space impl.
         *
         * @param socialSpace the social space
         * @throws RepositoryException the repository exception
         * @throws OutlookException the outlook exception
         */
        protected OutlookSpaceImpl(Space socialSpace) throws RepositoryException, OutlookException {
            super(socialSpace.getGroupId(), socialSpace.getDisplayName(), socialSpace.getShortName(),
                    socialSpace.getPrettyName());
            this.rootPath = groupDocsPath(groupId);
            this.socialIdentityManager = socialIdentityManager();
            this.socialActivityManager = socialActivityManager();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Folder getFolder(String path) throws OutlookException, RepositoryException {
            Folder parent = getRootFolder();
            Folder folder;
            String folderPath = HierarchyNode.getPath(path);
            if (rootPath.equals(folderPath)) {
                folder = parent;
            } else if (folderPath.startsWith(rootPath)) {
                folder = new SpaceFolder(parent, node(folderPath));
            } else {
                throw new BadParameterException("Path does not belong to space documents: " + path);
            }
            initDocumentLink(this, folder);
            return folder;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Folder getRootFolder() throws OutlookException, RepositoryException {
            RootFolder root = rootFolder.get();
            if (root != null) {
                // ensure folder's node valid
                try {
                    root.getNode().getIndex();
                    return root;
                } catch (InvalidItemStateException e) {
                    // it's invalid
                }
            }
            root = new RootFolder(rootPath, node(rootPath));
            rootFolder.set(root);
            return root;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<File> findLastDocuments(String text) throws RepositoryException, OutlookException {
            Folder root = getRootFolder();
            QueryManager qm = root.getNode().getSession().getWorkspace().getQueryManager();

            Set<File> res = new LinkedHashSet<File>();

            if (text == null || text.length() == 0) {
                Query qOwn = qm.createQuery(
                        "SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId() + "' AND jcr:path LIKE '"
                                + root.getPath() + "/%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC",
                        Query.SQL);
                // fetch first three modified by this user only
                fetchQuery(qOwn.execute(), 3, res);
                // and add all others up to total 20 files
                Query qOthers = qm.createQuery("SELECT * FROM nt:file WHERE jcr:path LIKE '" + root.getPath()
                        + "/%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL);
                fetchQuery(qOthers.execute(), 20 - res.size(), res);
            } else {
                Query qOwn = qm.createQuery("SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId()
                        + "' AND jcr:path LIKE '" + root.getPath() + "/%' AND exo:title LIKE '%" + text
                        + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL);
                // fetch first three modified by this user only
                fetchQuery(qOwn.execute(), 3, res);
                // and add all others up to total 20 files
                Query qOthers = qm.createQuery(
                        "SELECT * FROM nt:file WHERE jcr:path LIKE '" + root.getPath() + "/%' AND exo:title LIKE '%"
                                + text + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC",
                        Query.SQL);
                fetchQuery(qOthers.execute(), 20 - res.size(), res);
            }
            return res;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ExoSocialActivity postActivity(OutlookMessage message) throws Exception {
            Node spaceDocs = spaceDocumentsNode(groupId);
            Node messagesFolder = messagesFolder(spaceDocs, groupId);
            Node messageFile = addMessageFile(messagesFolder, message);
            setPermissions(messageFile, new StringBuilder("member:").append(groupId).toString());
            messagesFolder.save();
            message.setFileNode(messageFile);

            String userMessage = message.getTitle();
            userMessage = userMessage != null && userMessage.length() > 0 ? safeText(userMessage) : null;

            final String origType = org.exoplatform.wcm.ext.component.activity.listener.Utils.getActivityType();
            try {
                org.exoplatform.wcm.ext.component.activity.listener.Utils
                        .setActivityType(OutlookMessageActivity.ACTIVITY_TYPE);
                ExoSocialActivity activity = org.exoplatform.wcm.ext.component.activity.listener.Utils
                        .postFileActivity(messageFile, userMessage, true, false, "", "");
                // TODO should we care about activity removal with the message file?
                activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId()));
                return activity;
            } finally {
                org.exoplatform.wcm.ext.component.activity.listener.Utils.setActivityType(origType);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ExoSocialActivity postActivity(OutlookUser user, String title, String body) throws Exception {
            // post activity to space status stream under current user
            Identity spaceIdentity = socialIdentityManager.getOrCreateIdentity(SpaceIdentityProvider.NAME,
                    this.prettyName, true);
            Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME,
                    currentUserId(), true);
            String safeTitle = safeText(title);
            String safeBody = safeHtml(body);
            ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(),
                    SpaceActivityPublisher.SPACE_APP_ID, safeTitle, safeBody);
            socialActivityManager.saveActivityNoReturn(spaceIdentity, activity);
            activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId()));
            return activity;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public ExoSocialActivity postActivity(OutlookUser user, String text) throws Exception {
            // post activity to space status stream under current user
            Identity spaceIdentity = socialIdentityManager.getOrCreateIdentity(SpaceIdentityProvider.NAME,
                    this.prettyName, true);
            Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME,
                    currentUserId(), true);
            String safeText = safeActivityMessage(text);
            ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(),
                    SpaceActivityPublisher.SPACE_APP_ID, safeText, null);
            // we do like done UIDefaultActivityComposer
            activity.setType(UIDefaultActivity.ACTIVITY_TYPE);

            socialActivityManager.saveActivityNoReturn(spaceIdentity, activity);
            activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId()));
            return activity;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Page addWikiPage(OutlookMessage message) throws Exception {
            String wikiType = PortalConfig.GROUP_TYPE;
            String creator = message.getUser().getLocalUser();
            List<String> users = new ArrayList<String>();
            users.add(creator);
            // TODO add space group to users?
            return createWikiPage(wikiType, getGroupId(), creator, message.getSubject(), messageSummary(message),
                    message.getBody(), users);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Topic addForumTopic(OutlookMessage message) throws Exception {
            String creator = message.getUser().getLocalUser();

            //
            Group group = organization.getGroupHandler().findGroupById(getGroupId());
            String parentGrId = group.getParentId();

            // Category must exists as we are running against existing space
            String categoryId = org.exoplatform.forum.service.Utils.CATEGORY
                    + parentGrId.replaceAll(CommonUtils.SLASH, CommonUtils.EMPTY_STR);

            // Forum must exists as we are running against existing space
            String forumId = org.exoplatform.forum.service.Utils.FORUM_SPACE_ID_PREFIX + group.getGroupName();

            //
            return createForumTopic(categoryId, forumId, creator, message.getSubject(), messageSummary(message),
                    message.getBody());
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Topic addForumTopic(OutlookUser user, String name, String text) throws Exception {
            String creator = user.getLocalUser();

            //
            Group group = organization.getGroupHandler().findGroupById(getGroupId());
            String parentGrId = group.getParentId();

            // Category must exists as we are running against existing space
            String categoryId = org.exoplatform.forum.service.Utils.CATEGORY
                    + parentGrId.replaceAll(CommonUtils.SLASH, CommonUtils.EMPTY_STR);

            // Forum must exists as we are running against existing space
            String forumId = org.exoplatform.forum.service.Utils.FORUM_SPACE_ID_PREFIX + group.getGroupName();

            //
            return createForumTopic(categoryId, forumId, creator, name, null, text);
        }

    }

    /** The jcr service. */
    protected final RepositoryService jcrService;

    /** The session providers. */
    protected final SessionProviderService sessionProviders;

    /** The finder. */
    protected final NodeFinder finder;

    /** The hierarchy creator. */
    protected final NodeHierarchyCreator hierarchyCreator;

    /** The organization. */
    protected final OrganizationService organization;

    /** The drive service. */
    protected final ManageDriveService driveService;

    /** The listener service. */
    protected final ListenerService listenerService;

    /** The wiki service. */
    protected final WikiService wikiService;

    /** The forum service. */
    protected final ForumService forumService;

    /** The trash service. */
    protected final TrashService trashService;

    /** The wiki rendering service. */
    protected final RenderingService wikiRenderingService;

    /** The resource bundle service. */
    protected final ResourceBundleService resourceBundleService;

    /** The html policy. */
    protected final PolicyFactory htmlPolicy = Sanitizers.BLOCKS.and(Sanitizers.FORMATTING).and(Sanitizers.IMAGES)
            .and(Sanitizers.LINKS).and(Sanitizers.TABLES)
            // with
            // extra
            // attributes
            // for
            // tables
            // (MS
            // loves
            // to use
            // them
            // for HTML
            // re-formating)
            .and(new HtmlPolicyBuilder().allowStandardUrlProtocols().allowElements("table", "th", "tr", "td")
                    .allowAttributes("border", "cellpadding", "cellspacing", "width", "height").onElements("table")
                    .allowAttributes("bgcolor", "width", "height", "colspan", "rowspan")
                    .onElements("td", "tr", "th").toFactory())
            .and(Sanitizers.STYLES);

    /** The text policy. */
    protected final PolicyFactory textPolicy = new HtmlPolicyBuilder().toFactory();

    /**
     * Custom policy to allow supported elements in activity text as described in
     * <a href=
     * "https://www.exoplatform.com/docs/PLF43/PLFUserGuide.GettingStarted.ActivitiesInActivityStream.HTMLTags.html">
     * Platform User Guide</a>
     */
    protected final PolicyFactory activityPolicy = new HtmlPolicyBuilder().allowUrlProtocols("http", "https")
            .allowElements("b", "i", "a", "span", "em", "strong", "p", "ol", "ul", "li", "br", "img", "blockquote",
                    "q")
            .allowAttributes("href").onElements("a").allowAttributes("target").matching(true, "_blank")
            .onElements("a").allowAttributes("alt", "src").onElements("img").toFactory();

    /** The link with href not a hash in local document target. */
    protected final Pattern linkNotLocal = Pattern.compile("href=['\"][^#][.\\w\\W\\S]*?['\"]",
            Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

    /** The link with target. */
    protected final Pattern linkWithTarget = Pattern.compile("<a(?=\\s).*?(target=['\"].*?['\"])[^>]*>",
            Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

    /** The link without target. */
    protected final Pattern linkWithoutTarget = Pattern.compile(
            "<a(?=\\s)(?:(?!target=).)*?([.\\W\\w\\S\\s[^>]])*?(>)",
            Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

    /**
     * Authenticated users.
     */
    protected final ConcurrentHashMap<String, OutlookUser> authenticated = new ConcurrentHashMap<String, OutlookUser>();

    /**
     * Spaces cache.
     * TODO There is an issue with threads when different requests reuse them. Space's root node may be already
     * invalid. See also in getRootFolder().
     */
    protected final ConcurrentHashMap<String, OutlookSpaceImpl> spaces = new ConcurrentHashMap<String, OutlookSpaceImpl>();

    /** The mailserver api. */
    protected MailAPI mailserverApi;

    /** The trash home path. */
    protected String trashHomePath;

    /**
     * Outlook service with storage in JCR and with managed features.
     *
     * @param jcrService {@link RepositoryService}
     * @param sessionProviders {@link SessionProviderService}
     * @param hierarchyCreator {@link NodeHierarchyCreator}
     * @param finder {@link NodeFinder}
     * @param organization {@link OrganizationService}
     * @param listenerService {@link ListenerService}
     * @param driveService {@link ManageDriveService}
     * @param trashService {@link TrashService}
     * @param wikiService {@link WikiService}
     * @param forumService {@link ForumService}
     * @param wikiRenderingService {@link RenderingService}
     * @param resourceBundleService {@link ResourceBundleService}
     * @param params {@link InitParams}
     * @throws ConfigurationException when parameters configuration error
     * @throws MailServerException when Mail server API error
     */
    public OutlookServiceImpl(RepositoryService jcrService, SessionProviderService sessionProviders,
            NodeHierarchyCreator hierarchyCreator, NodeFinder finder, OrganizationService organization,
            ListenerService listenerService, ManageDriveService driveService, TrashService trashService,
            WikiService wikiService, ForumService forumService, RenderingService wikiRenderingService,
            ResourceBundleService resourceBundleService, InitParams params)
            throws ConfigurationException, MailServerException {

        this.jcrService = jcrService;
        this.sessionProviders = sessionProviders;
        this.hierarchyCreator = hierarchyCreator;
        this.finder = finder;
        this.organization = organization;
        this.driveService = driveService;
        this.listenerService = listenerService;
        this.wikiService = wikiService;
        this.forumService = forumService;
        this.trashService = trashService;
        this.wikiRenderingService = wikiRenderingService;
        this.resourceBundleService = resourceBundleService;

        // API for user requests (uses credentials from eXo user profile)
        MailAPI api = new MailAPI();
        this.mailserverApi = api;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Deprecated
    public Folder getFolder(String path) throws OutlookException, RepositoryException {
        Node node = node(path);
        Folder folder = new UserFolder(path, node);
        return folder;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Deprecated
    public Folder getFolder(Folder parent, String path) throws OutlookException, RepositoryException {
        Node node = node(path);
        return new UserFolder(parent, node);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<File> saveAttachment(OutlookSpace space, Folder destFolder, OutlookUser user, String comment,
            String messageId, String attachmentToken, String... attachmentIds)
            throws OutlookException, RepositoryException {
        List<File> files = new ArrayList<File>();
        Node parent = destFolder.getNode();
        for (String attachmentId : attachmentIds) {
            JsonValue vatt = mailserverApi.getAttachment(user, messageId, attachmentToken, attachmentId);
            JsonValue vName = vatt.getElement("Name");
            if (isNull(vName)) {
                throw new OutlookFormatException("Attachment doesn't contain Name");
            }
            String name = vName.getStringValue();
            JsonValue vContentType = vatt.getElement("ContentType");
            if (isNull(vContentType)) {
                throw new OutlookFormatException("Attachment (" + name + ") doesn't contain ContentType");
            }
            String contentType = vContentType.getStringValue();
            JsonValue vContentBytes = vatt.getElement("ContentBytes");
            if (isNull(vContentBytes)) {
                throw new OutlookFormatException("Attachment (" + name + ") doesn't contain ContentBytes");
            }
            // FYI attachment content in BASE64 (may be twice!)
            String contentBytes = vContentBytes.getStringValue();
            byte[] decoded = Base64.decodeBase64(contentBytes);

            // Save in JCR: content goes as-is here
            try (InputStream contentStream = new ByteArrayInputStream(decoded)) {
                Node attachmentNode = addFile(parent, name, contentType, contentStream);
                if (space != null) {
                    setPermissions(attachmentNode,
                            new StringBuilder("member:").append(space.getGroupId()).toString());
                } else {
                    setPermissions(attachmentNode, user.getLocalUser(), "member:/platform/users");
                }
                files.add(new UserFile(destFolder, attachmentNode));
            } catch (IOException e) {
                throw new OutlookException("Error saving attachment in a file " + name, e);
            }
        }
        parent.save(); // save everything at the end only

        // TODO care about activity removal with the message file
        postAttachmentActivity(destFolder, files, user, safeText(comment));

        if (space != null) {
            for (File f : files) {
                initDocumentLink(space, f);
            }
        }
        return files;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<File> saveAttachment(Folder destFolder, OutlookUser user, String comment, String messageId,
            String attachmentToken, String... attachmentIds) throws OutlookException, RepositoryException {
        return saveAttachment(null, destFolder, user, comment, messageId, attachmentToken, attachmentIds);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutlookEmail getAddress(String email, String displayName) throws OutlookException {
        return new OutlookEmail(email, displayName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutlookUser getUser(String email, String displayName, String ewsUrl)
            throws OutlookException, RepositoryException {
        ConversationState contextState = ConversationState.getCurrent();
        if (contextState != null) {
            String exoUsername = contextState.getIdentity().getUserId();
            if (!IdentityConstants.ANONIM.equals(exoUsername)) {
                URI mailServerUrl;
                if (ewsUrl != null) {
                    try {
                        URI ewsUri = new URI(ewsUrl);
                        String host = ewsUri.getHost();
                        String scheme = ewsUri.getScheme();
                        int port = ewsUri.getPort();
                        // TODO do we need remove obvious things?
                        // if (port >= 0) {
                        // if (port == 443 && "https".equalsIgnoreCase(scheme)) {
                        // port = -1;
                        // } else if (port == 80 && "http".equalsIgnoreCase(scheme)) {
                        // port = -1;
                        // }
                        // }

                        mailServerUrl = new URI(scheme, null, host, port, null, null, null);
                    } catch (URISyntaxException e) {
                        throw new MailServerException("Error parsing EWS API URL " + ewsUrl, e);
                    }
                } else {
                    mailServerUrl = null;
                }

                OutlookUser user = authenticated.get(exoUsername);
                if (user == null) {
                    // new user instance
                    user = new UserImpl(email, displayName, exoUsername);
                    // save user in map of authenticated for later use (multi-thread)
                    authenticated.put(exoUsername, user);
                }
                if (email != null) {
                    user.setEmail(email);
                }
                if (mailServerUrl != null) {
                    user.setMailServerUrl(mailServerUrl);
                }
                return user;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutlookMessage buildMessage(String id, OutlookUser user, OutlookEmail from, List<OutlookEmail> to,
            Calendar created, Calendar modified, String title, String subject, String body)
            throws OutlookException {
        OutlookMessage message = new OutlookMessage(user);
        message.setId(id);
        message.setFrom(from);
        message.setTo(to);
        message.setTitle(title);
        message.setSubject(subject);
        message.setBody(body);
        message.setCreated(created);
        message.setModified(modified);
        return message;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutlookMessage getMessage(OutlookUser user, String messageId, String messageToken)
            throws OutlookException {

        // Read message from Exchange server by ID
        JsonValue vatt = mailserverApi.getMessage(user, messageId, messageToken);
        JsonValue vSubject = vatt.getElement("Subject");
        if (isNull(vSubject)) {
            throw new OutlookFormatException("Message " + messageId + " doesn't contain Subject");
        }
        String subject = vSubject.getStringValue();
        JsonValue vTo = vatt.getElement("ToRecipients");
        if (isNull(vTo)) {
            throw new OutlookFormatException(
                    "Message (" + messageId + " : " + subject + ") doesn't contain ToRecipients");
        }
        if (!vTo.isArray()) {
            throw new OutlookFormatException(
                    "Message (" + messageId + " : " + subject + ")'s ToRecipients isn't an array");
        }
        List<OutlookEmail> to = new ArrayList<OutlookEmail>(vTo.size());
        for (Iterator<JsonValue> toiter = vTo.getElements(); toiter.hasNext();) {
            to.add(readEmail(toiter.next()));
        }
        OutlookEmail from;
        JsonValue vFrom = vatt.getElement("From");
        if (isNull(vFrom)) {
            from = null;
        } else {
            from = readEmail(vFrom);
        }
        JsonValue vCreatedDateTime = vatt.getElement("CreatedDateTime");
        if (isNull(vCreatedDateTime)) {
            throw new OutlookFormatException(
                    "Message (" + messageId + " : " + subject + ") doesn't contain CreatedDateTime");
        }
        Calendar created = Calendar.getInstance();
        try {
            created.setTime(OutlookMessage.DATE_FORMAT.parse(vCreatedDateTime.getStringValue()));
        } catch (ParseException e) {
            LOG.error("Error parsing message date " + vCreatedDateTime.getStringValue(), e);
        }
        JsonValue vLastModifiedDateTime = vatt.getElement("LastModifiedDateTime");
        if (isNull(vLastModifiedDateTime)) {
            throw new OutlookFormatException(
                    "Message (" + messageId + " : " + subject + ") doesn't contain LastModifiedDateTime");
        }
        Calendar modified = Calendar.getInstance();
        try {
            modified.setTime(OutlookMessage.DATE_FORMAT.parse(vLastModifiedDateTime.getStringValue()));
        } catch (ParseException e) {
            LOG.error("Error parsing message date " + vCreatedDateTime.getStringValue(), e);
        }
        JsonValue vBody = vatt.getElement("Body");
        if (isNull(vBody)) {
            throw new OutlookFormatException("Message (" + messageId + " : " + subject + ") doesn't contain Body");
        }
        JsonValue vContentType = vBody.getElement("ContentType");
        if (isNull(vContentType)) {
            throw new OutlookFormatException(
                    "Message (" + messageId + " : " + subject + ")'s body doesn't contain ContentType");
        }
        String contentType = vContentType.getStringValue();
        JsonValue vContent = vBody.getElement("Content");
        if (isNull(vContent)) {
            throw new OutlookFormatException(
                    "Message (" + messageId + " : " + subject + ")'s body doesn't contain Content");
        }
        String content = vContent.getStringValue();
        OutlookMessage message = new OutlookMessage(user);
        message.setId(messageId);
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setBody(content);
        message.setType(contentType);
        message.setCreated(created);
        message.setModified(modified);
        return message;
    }

    /**
     * On-start initializer.
     */
    @Override
    public void start() {
        try {
            this.trashHomePath = trashService.getTrashHomeNode().getPath();
        } catch (RepositoryException e) {
            LOG.warn("Error getting Trash home node", e);
            this.trashHomePath = "/Trash";
        }
        LOG.info("Outlook service successfuly started");
    }

    /**
     * On-stop finalizer.
     */
    @Override
    public void stop() {
        authenticated.clear();
        try {
            mailserverApi.close();
            LOG.info("Outlook service successfuly stopped");
        } catch (MailServerException e) {
            LOG.warn("Outlook service stop encountered with API error", e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutlookSpace getSpace(String groupId)
            throws OutlookSpaceException, RepositoryException, OutlookException {
        OutlookSpaceImpl space = spaces.get(groupId);
        if (space == null) {
            Space socialSpace = spaceService().getSpaceByGroupId(groupId);
            if (socialSpace != null) {
                space = new OutlookSpaceImpl(socialSpace);
                spaces.put(socialSpace.getGroupId(), space);
            }
        }
        return space;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<OutlookSpace> getUserSpaces() throws OutlookSpaceException {
        return userSpaces(currentUserId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UserDocuments getUserDocuments() throws RepositoryException, OutlookException {
        // get Personal Documents folder
        String userName = currentUserId();
        try {
            Node userDocsNode = userDocumentsNode(userName);
            PersonalDocumentsFolder folder = new PersonalDocumentsFolder(userDocsNode);
            initDocumentLink(folder, folder);
            return folder;
        } catch (Exception e) {
            throw new OutlookException("Error reading user's Personal Documents node for " + userName, e);
        }
    }

    // *********************** testing level **********************

    /**
     * Sets the api.
     *
     * @param mockedAPI the new api
     */
    void setAPI(MailAPI mockedAPI) {
        this.mailserverApi = mockedAPI;
    }

    // *********************** implementation level ***************

    /**
     * Node title.
     *
     * @param node the node
     * @return the string
     * @throws RepositoryException the repository exception
     */
    protected String nodeTitle(Node node) throws RepositoryException {
        return node.getProperty(NodetypeConstant.EXO_TITLE).getString();
    }

    /**
     * Node content.
     *
     * @param node the node
     * @return the node
     * @throws RepositoryException the repository exception
     */
    protected Node nodeContent(Node node) throws RepositoryException {
        return node.getNode("jcr:content");
    }

    /**
     * Node created.
     *
     * @param node the node
     * @return the calendar
     * @throws RepositoryException the repository exception
     */
    protected Calendar nodeCreated(Node node) throws RepositoryException {
        return node.getProperty("jcr:created").getDate();
    }

    /**
     * Mime type.
     *
     * @param content the content
     * @return the string
     * @throws RepositoryException the repository exception
     */
    protected String mimeType(Node content) throws RepositoryException {
        return content.getProperty("jcr:mimeType").getString();
    }

    /**
     * Data.
     *
     * @param content the content
     * @return the property
     * @throws RepositoryException the repository exception
     */
    protected Property data(Node content) throws RepositoryException {
        return content.getProperty("jcr:data");
    }

    /**
     * Generate id.
     *
     * @param workspace the workspace
     * @param path the path
     * @return the uuid
     */
    protected UUID generateId(String workspace, String path) {
        StringBuilder s = new StringBuilder();
        s.append(workspace);
        s.append(path);
        s.append(System.currentTimeMillis());
        s.append(String.valueOf(RANDOM.nextLong()));

        return UUID.nameUUIDFromBytes(s.toString().getBytes());
    }

    /**
     * Gets the exo user.
     *
     * @param userName the user name
     * @return the exo user
     * @throws OutlookException the outlook exception
     */
    protected org.exoplatform.services.organization.User getExoUser(String userName) throws OutlookException {
        try {
            return organization.getUserHandler().findUserByName(userName);
        } catch (Exception e) {
            throw new OutlookException("Error searching user " + userName, e);
        }
    }

    /**
     * Node.
     *
     * @param nodePath the node path
     * @return the node
     * @throws BadParameterException the bad parameter exception
     * @throws RepositoryException the repository exception
     */
    protected Node node(String nodePath) throws BadParameterException, RepositoryException {
        String workspace, path;
        if (nodePath.startsWith("/")) {
            workspace = jcrService.getCurrentRepository().getConfiguration().getDefaultWorkspaceName();
            path = nodePath;
        } else {
            // TODO it's a not used experimental thing, see also ContentLink component
            int i = nodePath.indexOf('/');
            if (i > 0) {
                workspace = nodePath.substring(0, i);
                path = nodePath.substring(i);
            } else {
                throw new BadParameterException("Invalid path " + nodePath);
            }
        }
        return node(workspace, path);
    }

    /**
     * Node.
     *
     * @param workspace the workspace
     * @param path the path
     * @return the node
     * @throws BadParameterException the bad parameter exception
     * @throws RepositoryException the repository exception
     */
    protected Node node(String workspace, String path) throws BadParameterException, RepositoryException {
        SessionProvider sp = sessionProviders.getSessionProvider(null);
        Session userSession = sp.getSession(workspace, jcrService.getCurrentRepository());

        Item item = finder.findItem(userSession, path);
        if (item.isNode()) {
            return (Node) item;
        } else {
            throw new BadParameterException("Not a node " + path);
        }
    }

    /**
     * System node.
     *
     * @param workspace the workspace
     * @param path the path
     * @return the node
     * @throws BadParameterException the bad parameter exception
     * @throws RepositoryException the repository exception
     */
    protected Node systemNode(String workspace, String path) throws BadParameterException, RepositoryException {
        SessionProvider sp = sessionProviders.getSystemSessionProvider(null);
        Session sysSession = sp.getSession(workspace, jcrService.getCurrentRepository());

        Item item = finder.findItem(sysSession, path);
        if (item.isNode()) {
            return (Node) item;
        } else {
            throw new BadParameterException("Not a node " + path);
        }
    }

    /**
     * Checkout.
     *
     * @param node the node
     * @return true, if successful
     * @throws RepositoryException the repository exception
     */
    protected boolean checkout(Node node) throws RepositoryException {
        if (node.isNodeType("mix:versionable")) {
            if (!node.isCheckedOut()) {
                node.checkout();
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if is null.
     *
     * @param json the json
     * @return true, if is null
     */
    protected boolean isNull(JsonValue json) {
        return json == null || json.isNull();
    }

    /**
     * Checks if is not null.
     *
     * @param json the json
     * @return true, if is not null
     */
    protected boolean isNotNull(JsonValue json) {
        return json != null && !json.isNull();
    }

    /**
     * Current user locale.
     *
     * @return the locale
     */
    protected Locale currentUserLocale() {
        WebuiRequestContext context = WebuiRequestContext.getCurrentInstance();
        return context != null ? context.getLocale() : null;
    }

    /**
     * Add nt:file node for given content stream and title. If a node with such name exists a new name will be
     * generated by adding a numerical index to the end.
     * 
     * @param parent {@link Node}
     * @param title {@link String}
     * @param contentType {@link String}
     * @param content {@link InputStream}
     * @return {@link Node}
     * @throws RepositoryException when storage error
     */
    protected Node addFile(Node parent, String title, String contentType, InputStream content)
            throws RepositoryException {
        Node file;
        String baseName = cleanName(title);
        String name = baseName;

        int siblingNumber = 0;
        do {
            try {
                file = parent.getNode(name);
                // such node already exists - find new name for the file (by adding sibling index to the end)
                siblingNumber++;
                int extIndex = baseName.lastIndexOf(".");
                if (extIndex > 0 && extIndex != baseName.length() - 1) {
                    String jcrName = baseName.substring(0, extIndex);
                    String jcrExt = baseName.substring(extIndex + 1);
                    name = new StringBuilder(jcrName).append('-').append(siblingNumber).append('.').append(jcrExt)
                            .toString();
                } else {
                    name = new StringBuilder(baseName).append('-').append(siblingNumber).toString();
                }
            } catch (PathNotFoundException e) {
                // no such node exists, add it using internalName created by CD's cleanName()
                file = parent.addNode(name, "nt:file");
                break;
            }
        } while (true);

        Node resource = file.addNode("jcr:content", "nt:resource");
        resource.setProperty("jcr:mimeType",
                contentType != null ? contentType : ContentType.APPLICATION_OCTET_STREAM.getMimeType());
        Calendar fileDate = Calendar.getInstance();
        resource.setProperty("jcr:lastModified", fileDate);
        resource.setProperty("jcr:data", content);

        if (siblingNumber > 0) {
            int extIndex = title.lastIndexOf(".");
            if (extIndex > 0 && extIndex != title.length() - 1) {
                String titleName = title.substring(0, extIndex);
                String titleExt = title.substring(extIndex + 1);
                title = new StringBuilder(titleName).append(" (").append(siblingNumber).append(").")
                        .append(titleExt).toString();
            } else {
                title = new StringBuilder(title).append(" (").append(siblingNumber).append(')').toString();
            }
        }

        if (!file.hasProperty(NodetypeConstant.EXO_TITLE)) {
            file.addMixin(NodetypeConstant.EXO_RSS_ENABLE);
        }
        file.setProperty(NodetypeConstant.EXO_TITLE, title);
        // file.setProperty("exo:summary", summary);
        try {
            file.setProperty(NodetypeConstant.EXO_NAME, title);
        } catch (ConstraintViolationException | ValueFormatException e) {
            LOG.warn("Cannot set exo:name property to '" + title + "' for file " + file.getPath() + ": " + e);
        }

        if (file.isNodeType(NodetypeConstant.EXO_DATETIME)) {
            file.setProperty(NodetypeConstant.EXO_DATE_CREATED, fileDate);
            file.setProperty(NodetypeConstant.EXO_DATE_MODIFIED, fileDate);
        }

        if (file.isNodeType(NodetypeConstant.EXO_MODIFY)) {
            file.setProperty(NodetypeConstant.EXO_LAST_MODIFIED_DATE, fileDate);
            file.setProperty(NodetypeConstant.EXO_LAST_MODIFIER, file.getSession().getUserID());
        }

        // Added when upgraded to PLF 4.4
        if (!file.isNodeType(NodetypeConstant.MIX_REFERENCEABLE)) {
            file.addMixin(NodetypeConstant.MIX_REFERENCEABLE);
        }

        if (!file.isNodeType(NodetypeConstant.MIX_COMMENTABLE)) {
            file.addMixin(NodetypeConstant.MIX_COMMENTABLE);
        }

        if (!file.isNodeType(NodetypeConstant.MIX_VOTABLE)) {
            file.addMixin(NodetypeConstant.MIX_VOTABLE);
        }

        if (!file.isNodeType(NodetypeConstant.MIX_I18N)) {
            file.addMixin(NodetypeConstant.MIX_I18N);
        }

        return file;
    }

    /**
     * Add nt:folder node with given title. If a node with such name exists and <code>forceNew</code> is
     * <code>true</code> a new name will be generated by adding a numerical index to the end, otherwise existing
     * node will be returned.
     * 
     * @param parent {@link Node}
     * @param title {@link String}
     * @param forceNew if <code>true</code> then a new folder will be created with index in suffix, if
     *          <code>false</code> then existing folder will be returned
     * @return {@link Node}
     * @throws RepositoryException when storage error
     */
    protected Node addFolder(Node parent, String title, boolean forceNew) throws RepositoryException {
        Node folder;
        String baseName = cleanName(title);
        String name = baseName;

        int siblingNumber = 0;
        do {
            try {
                folder = parent.getNode(name);
                if (forceNew) {
                    // such node already exists - find new name for the file (by adding sibling index to the end)
                    siblingNumber++;
                    int extIndex = baseName.lastIndexOf(".");
                    if (extIndex > 0 && extIndex < title.length()) {
                        String jcrName = baseName.substring(0, extIndex);
                        String jcrExt = baseName.substring(extIndex + 1);
                        name = new StringBuilder(jcrName).append('-').append(siblingNumber).append('.')
                                .append(jcrExt).toString();
                    } else {
                        name = new StringBuilder(baseName).append('-').append(siblingNumber).toString();
                    }
                } else {
                    break;
                }
            } catch (PathNotFoundException e) {
                // no such node exists, add it using internalName created by CD's cleanName()
                folder = parent.addNode(name, "nt:folder");
                break;
            }
        } while (true);

        if (folder.isNew()) {
            if (siblingNumber > 0) {
                int extIndex = title.lastIndexOf(".");
                if (extIndex > 0 && extIndex < title.length()) {
                    String titleName = title.substring(0, extIndex);
                    String titleExt = title.substring(extIndex + 1);
                    title = new StringBuilder(titleName).append(" (").append(siblingNumber).append(").")
                            .append(titleExt).toString();
                } else {
                    title = new StringBuilder(title).append(" (").append(siblingNumber).append(')').toString();
                }
            }

            folder.setProperty(NodetypeConstant.EXO_TITLE, title);
            try {
                folder.setProperty(NodetypeConstant.EXO_NAME, title);
            } catch (ConstraintViolationException | ValueFormatException e) {
                LOG.warn("Cannot set exo:name property to '" + title + "' for folder " + folder.getPath() + ": "
                        + e);
            }

            Calendar folderDate = Calendar.getInstance();
            if (folder.isNodeType(NodetypeConstant.EXO_DATETIME)) {
                folder.setProperty(NodetypeConstant.EXO_DATE_CREATED, folderDate);
                folder.setProperty(NodetypeConstant.EXO_DATE_MODIFIED, folderDate);
            }

            if (folder.isNodeType(NodetypeConstant.EXO_MODIFY)) {
                folder.setProperty(NodetypeConstant.EXO_LAST_MODIFIED_DATE, folderDate);
                folder.setProperty(NodetypeConstant.EXO_LAST_MODIFIER, folder.getSession().getUserID());
            }
        }
        return folder;
    }

    /**
     * Current user id.
     *
     * @return the string
     */
    protected String currentUserId() {
        ConversationState contextState = ConversationState.getCurrent();
        if (contextState != null) {
            return contextState.getIdentity().getUserId();
        }
        return IdentityConstants.ANONIM;
    }

    /**
     * Space service.
     *
     * @return the space service
     */
    protected SpaceService spaceService() {
        return (SpaceService) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(SpaceService.class);
    }

    /**
     * Social identity manager.
     *
     * @return the identity manager
     */
    protected IdentityManager socialIdentityManager() {
        return (IdentityManager) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(IdentityManager.class);
    }

    /**
     * Social activity manager.
     *
     * @return the activity manager
     */
    protected ActivityManager socialActivityManager() {
        return (ActivityManager) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(ActivityManager.class);
    }

    /**
     * User spaces.
     *
     * @param userId the user id
     * @return the list
     * @throws OutlookSpaceException the outlook space exception
     */
    protected List<OutlookSpace> userSpaces(String userId) throws OutlookSpaceException {
        List<OutlookSpace> spaces = new ArrayList<OutlookSpace>();
        ListAccess<Space> list = spaceService().getMemberSpaces(userId);
        try {
            for (Space socialSpace : list.load(0, list.getSize())) {
                spaces.add(new OutlookSpaceImpl(socialSpace));
            }
            return spaces;
        } catch (Throwable e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Error loading user spaces", e);
            }
            throw new OutlookSpaceException("Error loading user spaces", e);
        }
    }

    /**
     * Generate the group documents (as /Groups/spaces/$SPACE_GROUP_ID/Documents).<br>
     * 
     * @param groupId {@link String}
     * @return {@link String}
     */
    protected String groupDocsPath(String groupId) {
        // XXX we do here as ECMS does in ManageDriveServiceImpl
        return GROUP_DRIVE_PARRTEN.replace("${groupId}", groupId);
    }

    /**
     * Generate the user documents (as /Users/${userId}/Private/Documents).<br>
     * 
     * @param userId {@link String}
     * @return {@link String}
     */
    protected String userDocsPath(String userId) {
        // XXX we do here as ECMS does in ManageDriveServiceImpl
        return PERSONAL_DRIVE_PARRTEN.replace("${userId}", userId) + "/Documents";
    }

    /**
     * Generate the group node path (as /Groups/spaces/$SPACE_GROUP_ID).<br>
     * 
     * @param groupId {@link String}
     * @return {@link String}
     * @throws Exception when error
     */
    protected String groupPath(String groupId) throws Exception {
        String groupsPath = hierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH);
        return groupsPath + groupId;
    }

    /**
     * Inits the web DAV link.
     *
     * @param node the node
     * @throws OutlookException the outlook exception
     */
    protected void initWebDAVLink(HierarchyNode node) throws OutlookException {
        // WebDAV URL
        try {
            node.setWebdavUrl(org.exoplatform.wcm.webui.Utils.getWebdavURL(node.getNode(), false, true));
        } catch (Exception e) {
            throw new OutlookException("Error generating WebDav URL for node " + node.getFullPath(), e);
        }
    }

    /**
     * Inits the document link.
     *
     * @param siteType the site type
     * @param driveName the drive name
     * @param portalName the portal name
     * @param nodeURI the node URI
     * @param node the node
     * @throws OutlookException the outlook exception
     */
    protected void initDocumentLink(SiteType siteType, String driveName, String portalName, String nodeURI,
            HierarchyNode node) throws OutlookException {
        // WebDAV URL
        initWebDAVLink(node);

        // Portal URL
        // Code adapted from ECMS's PermlinkActionComponent.getPermlink()

        String npath = node.getPath().replaceAll("/+", "/");

        String path = new StringBuilder().append(driveName).append(npath).toString();
        PortalRequestContext portalRequest = Util.getPortalRequestContext();
        if (portalRequest != null) {
            NodeURL nodeURL = portalRequest.createURL(NodeURL.TYPE);
            NavigationResource resource = new NavigationResource(siteType, portalName, nodeURI);
            nodeURL.setResource(resource);
            nodeURL.setQueryParameterValue("path", path);

            HttpServletRequest request = portalRequest.getRequest();
            try {
                URI requestUri = new URI(request.getScheme(), null, request.getServerName(),
                        request.getServerPort(), null, null, null);

                StringBuilder url = new StringBuilder();
                url.append(requestUri.toASCIIString());
                url.append(nodeURL.toString());
                node.setUrl(url.toString());
            } catch (URISyntaxException e) {
                throw new OutlookException("Error creating server URL " + request.getRequestURI().toString(), e);
            }
        } else {
            LOG.warn("Portal request not found. Node URL will be its WebDAV link. Node: " + node.getPath());
            node.setUrl(node.getWebdavUrl());
        }
    }

    /**
     * Inits the document link.
     *
     * @param space the space
     * @param file the file
     * @throws OutlookException the outlook exception
     */
    protected void initDocumentLink(OutlookSpace space, HierarchyNode file) throws OutlookException {
        // WebDAV URL
        initWebDAVLink(file);

        // Portal URL
        // We need like the following:
        // https://peter.exoplatform.com.ua:8443/portal/g/:spaces:product_team/product_team/documents?path=.spaces.product_team/Groups/spaces/product_team/Documents/uploads/page_management_https_loading.png
        initDocumentLink(SiteType.GROUP, // GROUP
                space.getGroupId().replace("/", "."), space.getGroupId(), // /spaces/product_team
                space.getShortName() + "/documents", // product_team/documents
                file);
    }

    /**
     * Inits the document link.
     *
     * @param personalDocuments the personal documents
     * @param file the file
     * @throws OutlookException the outlook exception
     */
    protected void initDocumentLink(PersonalDocumentsFolder personalDocuments, HierarchyNode file)
            throws OutlookException {
        // WebDAV URL
        initWebDAVLink(file);

        // Portal URL
        // We need like the following:
        // https://peter.exoplatform.com.ua:8443/portal/intranet/documents?path=Personal%20Documents/Users/j___/jo___/joh___/john/Private/Documents
        initDocumentLink(SiteType.PORTAL, // PORTAL
                personalDocuments.getDriveName(), "intranet", // intranet
                "documents", // documents
                file);
    }

    /**
     * Find given user Personal Documents folder using user session.
     * 
     * @param userName {@link String}
     * @return {@link Node} Personal Documents folder node or <code>null</code>
     * @throws Exception when error
     */
    protected Node userDocumentsNode(String userName) throws Exception {
        // code idea based on ECMS's UIJCRExplorerPortlet.getUserDrive()
        for (DriveData userDrive : driveService.getPersonalDrives(userName)) {
            String homePath = userDrive.getHomePath();
            if (homePath.endsWith("/Private")) {
                String driveRootPath = org.exoplatform.services.cms.impl.Utils.getPersonalDrivePath(homePath,
                        userName);
                return node(driveRootPath);
            }
        }
        return null;
    }

    /**
     * Find given group Documents folder using current user session.
     *
     * @param groupId {@link String}
     * @return {@link Node} space's Documents folder node or <code>null</code>
     * @throws Exception when error
     */
    protected Node spaceDocumentsNode(String groupId) throws Exception {
        return node(groupDocsPath(groupId));
    }

    /**
     * Set read permissions on the target node to all given identities (e.g. space group members). If node not
     * yet <code>exo:privilegeable</code> it will add such mixin to allow set the permissions first. Requested
     * permissions will not be set to the children nodes.<br>
     * 
     * @param node {@link Node} link target node
     * @param identities array of {@link String} with user identifiers (names or memberships)
     * @throws AccessControlException when access error
     * @throws RepositoryException when storage error
     */
    protected void setPermissions(Node node, String... identities)
            throws AccessControlException, RepositoryException {
        setPermissions(node, true, false, identities);
    }

    /**
     * Set read permissions on the target node to all given identities (e.g. space group members). Permissions
     * will not be set if target not <code>exo:privilegeable</code> and <code>forcePrivilegeable</code> is
     * <code>false</code>. If <code>deep</code> is <code>true</code> the target children nodes will be checked
     * also for a need to set the requested permissions. <br>
     * 
     * @param node {@link Node} link target node
     * @param deep {@link Boolean} if <code>true</code> then also children nodes will be set to the requested
     *          permissions
     * @param forcePrivilegeable {@link Boolean} if <code>true</code> and node not yet
     *          <code>exo:privilegeable</code> it will add such mixin to allow set the permissions.
     * @param identities array of {@link String} with user identifiers (names or memberships)
     * @throws AccessControlException when access error
     * @throws RepositoryException when storage error
     */
    protected void setPermissions(Node node, boolean deep, boolean forcePrivilegeable, String... identities)
            throws AccessControlException, RepositoryException {
        ExtendedNode target = (ExtendedNode) node;
        boolean setPermissions = true;
        if (target.canAddMixin(EXO_PRIVILEGEABLE)) {
            if (forcePrivilegeable) {
                target.addMixin(EXO_PRIVILEGEABLE);
            } else {
                // will not set permissions on this node, but will check the child nodes
                setPermissions = false;
            }
        } // else, already exo:privilegeable
        if (setPermissions) {
            for (String identity : identities) {
                String[] ids = identity.split(":");
                if (ids.length == 2) {
                    // it's group and we want allow given identity read only and additionally let managers remove the
                    // link
                    String managerMembership;
                    try {
                        MembershipType managerType = organization.getMembershipTypeHandler()
                                .findMembershipType("manager");
                        managerMembership = managerType.getName();
                    } catch (Exception e) {
                        LOG.error("Error finding manager membership in organization service. "
                                + "Will use any (*) to allow remove shared cloud file link", e);
                        managerMembership = "*";
                    }
                    target.setPermission(new StringBuilder(managerMembership).append(':').append(ids[1]).toString(),
                            MANAGER_PERMISSION);
                    target.setPermission(identity, READER_PERMISSION);
                } else {
                    // in other cases, we assume it's user identity and user should be able to remove the node
                    target.setPermission(identity, MANAGER_PERMISSION);
                }
            }
        }
        if (deep) {
            // check the all children also, but don't force adding exo:privilegeable
            for (NodeIterator niter = target.getNodes(); niter.hasNext();) {
                Node child = niter.nextNode();
                setPermissions(child, true, false, identities);
            }
        }
    }

    /**
     * Messages folder.
     *
     * @param parent the parent
     * @param identity the identity
     * @return the node
     * @throws RepositoryException the repository exception
     */
    protected Node messagesFolder(Node parent, String... identity) throws RepositoryException {
        Node messagesFolder;
        if (!parent.hasNode("outlook-messages")) {
            messagesFolder = parent.addNode(OUTLOOK_MESSAGES_NAME, "nt:folder");
            messagesFolder.setProperty("exo:title", OUTLOOK_MESSAGES_TITLE);
            try {
                messagesFolder.setProperty("exo:name", OUTLOOK_MESSAGES_TITLE);
            } catch (ConstraintViolationException | ValueFormatException e) {
                LOG.warn("Cannot set exo:name property for folder " + messagesFolder.getPath() + ": " + e);
            }
            if (identity != null) {
                setPermissions(messagesFolder, identity);
            }
            parent.save();
        } else {
            messagesFolder = parent.getNode(OUTLOOK_MESSAGES_NAME);
        }
        return messagesFolder;
    }

    /**
     * Adds the message file.
     *
     * @param parent the parent
     * @param message the message
     * @return the node
     * @throws RepositoryException the repository exception
     * @throws UnsupportedEncodingException the unsupported encoding exception
     * @throws IOException Signals that an I/O exception has occurred.
     */
    protected Node addMessageFile(Node parent, OutlookMessage message)
            throws RepositoryException, UnsupportedEncodingException, IOException {
        String safeTitle = safeText(message.getSubject());
        String safeContent = safeHtml(message.getBody());
        try (InputStream content = new ByteArrayInputStream(safeContent.getBytes("UTF-8"))) {
            // message file goes w/o summary, it will be generated in UI (OutlookMessageActivity)
            Node messageFile = addFile(parent, safeTitle, "text/html", content);
            messageFile.addMixin(MESSAGE_NODETYPE);
            messageFile.setProperty("mso:userEmail", message.getUser().getEmail());
            messageFile.setProperty("mso:userName", message.getUser().getDisplayName());
            messageFile.setProperty("mso:fromEmail", message.getFrom().getEmail());
            messageFile.setProperty("mso:fromName", message.getFrom().getDisplayName());
            // messageFile.setProperty("mso:toEmail", userEmail);
            messageFile.setProperty("mso:created", message.getCreated());
            messageFile.setProperty("mso:modified", message.getModified());
            messageFile.setProperty("mso:messageId", message.getId());
            return messageFile;
        }
    }

    /**
     * Post attachment activity.
     *
     * @param destFolder the dest folder
     * @param files the files
     * @param user the user
     * @param comment the comment
     * @return the exo social activity
     * @throws RepositoryException the repository exception
     */
    protected ExoSocialActivity postAttachmentActivity(Folder destFolder, List<File> files, OutlookUser user,
            String comment) throws RepositoryException {
        String author = user.getLocalUser();

        // FYI Code inspired by UIDocActivityComposer
        Map<String, String> activityParams = new LinkedHashMap<String, String>();

        StringBuilder filesLine = new StringBuilder();
        for (File f : files) {
            if (filesLine.length() > 0) {
                filesLine.append(',');
            }
            filesLine.append(OutlookAttachmentActivity.attachmentString(f.getNode().getUUID(), f.getTitle()));
        }

        activityParams.put(OutlookAttachmentActivity.FILES, filesLine.toString());
        activityParams.put(OutlookAttachmentActivity.WORKSPACE,
                destFolder.getNode().getSession().getWorkspace().getName());
        activityParams.put(OutlookAttachmentActivity.COMMENT, comment);
        activityParams.put(OutlookAttachmentActivity.AUTHOR, author);

        Calendar activityDate = Calendar.getInstance();
        DateFormat dateFormatter = new SimpleDateFormat(ISO8601.SIMPLE_DATETIME_FORMAT);
        String dateString = dateFormatter.format(activityDate.getTime());
        activityParams.put(OutlookAttachmentActivity.DATE_CREATED, dateString);
        activityParams.put(OutlookAttachmentActivity.DATE_LAST_MODIFIED, dateString);

        // if NT_FILE
        // activityParams.put(UIDocActivity.ID, node.isNodeType(NodetypeConstant.MIX_REFERENCEABLE) ?
        // node.getUUID() : "");
        // activityParams.put(UIDocActivity.CONTENT_NAME, node.getName());
        // activityParams.put(UIDocActivity.AUTHOR, activityOwnerId);
        // activityParams.put(UIDocActivity.DATE_CREATED, strDateCreated);
        // activityParams.put(UIDocActivity.LAST_MODIFIED, strLastModified);
        // activityParams.put(UIDocActivity.CONTENT_LINK, UIDocActivity.getContentLink(node));

        //
        IdentityManager identityManager = socialIdentityManager();
        Identity authorIdentity = identityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, author,
                true);

        //
        String title = comment != null && comment.length() > 0 ? comment
                : new StringBuilder("User ").append(author).append(" has saved ").append(files.size())
                        .append(files.size() > 1 ? " files" : " file").toString();
        ExoSocialActivity activity = new ExoSocialActivityImpl(authorIdentity.getId(),
                OutlookAttachmentActivity.ACTIVITY_TYPE, title, null);
        activity.setTemplateParams(activityParams);

        // activity destination (user or space)
        ActivityManager activityManager = socialActivityManager();
        String spaceGroupName = getSpaceName(destFolder.getNode());
        Space space = spaceService().getSpaceByGroupId(SpaceUtils.SPACE_GROUP + "/" + spaceGroupName);
        if (spaceGroupName != null && spaceGroupName.length() > 0 && space != null) {
            // post activity to space stream
            Identity spaceIdentity = identityManager.getOrCreateIdentity(SpaceIdentityProvider.NAME,
                    space.getPrettyName(), true);
            activityManager.saveActivityNoReturn(spaceIdentity, activity);
        } else {
            // post activity to user status stream
            activityManager.saveActivityNoReturn(authorIdentity, activity);
        }

        //
        activity = activityManager.getActivity(activity.getId());
        return activity;
    }

    /**
     * Read email.
     *
     * @param vElem the v elem
     * @return the outlook email
     * @throws OutlookException the outlook exception
     */
    protected OutlookEmail readEmail(JsonValue vElem) throws OutlookException {
        JsonValue vEmailAddress = vElem.getElement("EmailAddress");
        if (isNull(vEmailAddress)) {
            throw new OutlookFormatException("Element doesn't contain EmailAddress");
        }
        String email;
        JsonValue vAddress = vEmailAddress.getElement("Address");
        if (isNull(vAddress)) {
            throw new OutlookFormatException("Element doesn't contain Address");
        } else {
            email = vAddress.getStringValue();
        }
        String name;
        JsonValue vName = vEmailAddress.getElement("Name");
        if (isNull(vName)) {
            name = "".intern();
        } else {
            name = vName.getStringValue();
        }
        return getAddress(email, name);
    }

    /**
     * Fetch query.
     *
     * @param qr the qr
     * @param limit the limit
     * @param res the res
     * @throws RepositoryException the repository exception
     * @throws OutlookException the outlook exception
     */
    protected void fetchQuery(QueryResult qr, int limit, Set<File> res)
            throws RepositoryException, OutlookException {
        fetchQuery(qr, limit, res, n -> true);
    }

    /**
     * Fetch query.
     *
     * @param qr the qr
     * @param limit the limit
     * @param res the res
     * @param acceptNode the accept node
     * @throws RepositoryException the repository exception
     * @throws OutlookException the outlook exception
     */
    protected void fetchQuery(QueryResult qr, int limit, Set<File> res, Predicate<Node> acceptNode)
            throws RepositoryException, OutlookException {
        SpaceService spaceService = spaceService();
        for (NodeIterator niter = qr.getNodes(); niter.getPosition() < limit && niter.hasNext();) {
            Node node = niter.nextNode();
            try {
                if (acceptNode.test(node)) {
                    // detect is it space and then check if space member
                    String path = node.getPath();
                    Space space;
                    if (path.startsWith(SPACES_HOME)) {
                        try {
                            String groupId = path.substring(7, path.indexOf("/", SPACES_HOME.length() + 1));
                            space = spaceService.getSpaceByGroupId(groupId);
                            if (space != null) {
                                Set<String> allMemembers = new HashSet<String>();
                                for (String s : space.getManagers()) {
                                    allMemembers.add(s);
                                }
                                for (String s : space.getMembers()) {
                                    allMemembers.add(s);
                                }
                                if (!allMemembers.contains(currentUserId())) {
                                    // when not a space member - skip this file (but user still may be an owner of it!)
                                    limit++;
                                    continue;
                                }
                            }
                        } catch (IndexOutOfBoundsException e) {
                            // XXX something not clear with space path, will use portal page path as for Personal
                            // Documents (it works well in PLF 4.3)
                            space = null;
                        }
                    } else {
                        space = null;
                    }

                    UserFile file = new UserFile(node.getParent().getPath(), node);
                    if (space != null) {
                        initDocumentLink(SiteType.GROUP, // GROUP
                                space.getGroupId().replace("/", "."), space.getGroupId(), // /spaces/product_team
                                space.getShortName() + "/documents", // product_team/documents
                                file);
                    } else {
                        initDocumentLink(SiteType.PORTAL, // PORTAL
                                PERSONAL_DOCUMENTS, "intranet", // intranet
                                "documents", // documents
                                file);
                    }
                    res.add(file);
                } else {
                    limit++;
                }
            } catch (RepositoryException e) {
                LOG.warn("Error read queried node " + e.getMessage() + ". Node skipped: " + node);
                limit++;
            }
        }

    }

    /**
     * Method adapted from eXo Chat's WikiService.createOrEditPage().
     * 
     * @param wikiType {@link String}
     * @param wikiOwner {@link String}
     * @param creator {@link String}
     * @param title {@link String}
     * @param summary {@link String}
     * @param content {@link String}
     * @param users list of {@link String}
     * @return {@link Page}
     * @throws Exception when error
     */
    protected Page createWikiPage(String wikiType, String wikiOwner, String creator, String title, String summary,
            String content, List<String> users) throws Exception {
        final String parentTitle = OUTLOOK_MESSAGES_TITLE;
        final String parentId = TitleResolver.getId(parentTitle, false);

        final String defaultSyntax = wikiService.getDefaultWikiSyntaxId(); // Syntax.XWIKI_2_0.toIdString()
        final String xhtmlSyntax = Syntax.XHTML_1_0.toIdString();

        synchronized (wikiService) {
            Page parentPage = wikiService.getPageOfWikiByName(wikiType, wikiOwner, parentId);
            if (parentPage == null) {
                parentPage = new Page();
                parentPage.setTitle(parentTitle);
                parentPage.setContent("= " + parentTitle + " =\n");
                parentPage.setSyntax(defaultSyntax);
                Wiki wiki = wikiService.getWikiByTypeAndOwner(wikiType, wikiOwner);
                if (wiki == null) {
                    wiki = wikiService.createWiki(wikiType, wikiOwner);
                }
                Page wikiHome = wiki.getWikiHome();
                setPermissionForWikiPage(Collections.<String>emptyList(), parentPage, wikiHome);
                List<PermissionEntry> permissions = parentPage.getPermissions();
                permissions.add(new PermissionEntry(WIKI_PERMISSION_ANY, "", IDType.USER, new Permission[] {
                        new Permission(org.exoplatform.wiki.mow.api.PermissionType.VIEWPAGE, true) }));
                parentPage.setPermissions(permissions);
                Wiki pwiki = new Wiki();
                pwiki.setOwner(wikiOwner);
                pwiki.setType(wikiType);
                parentPage = wikiService.createPage(pwiki, "WikiHome", parentPage);
            }

            Wiki wiki = new Wiki();
            wiki.setOwner(wikiOwner);
            wiki.setType(wikiType);

            Page page = new Page();

            if (isHTML(content) && !defaultSyntax.equals(xhtmlSyntax)) {
                // we use xWiki syntax for a page and convert incoming HTML to xWiki format
                page.setSyntax(defaultSyntax);
                StringBuilder quotedContent = new StringBuilder();
                if (summary != null) {
                    // page also contains message summary (US_003_07)
                    quotedContent.append("(% style='word-wrap: break-word; min-height: 30px;' %)(((\r");
                    String xwikiMarkup = wikiRenderingService.render(summary, xhtmlSyntax, defaultSyntax, false);
                    quotedContent.append(xwikiMarkup);
                    quotedContent.append("\r)))\r");
                }
                // message content should look like a quoted in email client (vertical gray bar on the left)
                // in xWiki syntax it's a table with customized style
                // table header:
                quotedContent.append("|=(% style='background-color: #999999; border-style: hidden;' %)");
                quotedContent.append("|=(% style='background-color: inherit; border-style: hidden;' %)");
                quotedContent.append("|=(% style='background-color: inherit; border-style: hidden;' %)\r");
                // table row:
                quotedContent.append("|(% style='background-color: #999999; border-style: hidden;' %) ");
                quotedContent.append("|(% style='border-style: hidden;' %) ");
                quotedContent.append("|(% style='border-style: hidden;' %) (((\r");
                String xwikiMarkup = wikiRenderingService.render(safeHtml(content), xhtmlSyntax, defaultSyntax,
                        false);
                quotedContent.append(xwikiMarkup);
                quotedContent.append("\r)))\r");
                page.setContent(quotedContent.toString());
            } else {
                page.setSyntax(defaultSyntax);
                page.setContent(content);
            }

            setPermissionForWikiPage(users, page, parentPage);
            page.setOwner(creator);
            page.setAuthor(creator);
            page.setMinorEdit(false);

            //
            title = safeText(title);
            String baseTitle = title;

            int siblingNumber = 0;
            do {
                String pageId = TitleResolver.getId(title, false);
                page.setTitle(title);
                String path = "";
                if (wikiType.equals(PortalConfig.GROUP_TYPE)) {
                    // http://demo.exoplatform.net/portal/intranet/wiki/group/spaces/bank_project/Meeting_06-11-2013
                    path = "/portal/intranet/wiki/" + wikiType + wikiOwner + "/" + pageId;
                } else if (wikiType.equals(PortalConfig.PORTAL_TYPE)) {
                    // http://demo.exoplatform.net/portal/intranet/wiki/Sales_Meetings_Meeting_06-11-2013
                    path = "/portal/intranet/wiki/" + pageId;
                }
                page.setUrl(path);

                if (!wikiService.isExisting(wikiType, wikiOwner, pageId)) {
                    try {
                        page = wikiService.createPage(wiki, parentId, page);
                        break;
                    } catch (WikiException e) {
                        LOG.warn("Error creating wiki page " + title + " (" + pageId + "). " + e.getMessage());
                        try {
                            wikiService.getPageById(pageId);
                        } catch (WikiException ge) {
                            // if we caught error here - we thrown a first one of the creation
                            throw e;
                        }
                    }
                }

                // such page already exists - find new name for it (by adding sibling index to the end)
                siblingNumber++;
                title = new StringBuilder(baseTitle).append(" (").append(siblingNumber).append(')').toString();
            } while (true);

            return page;
        }
    }

    /**
     * Generate message summary text.
     * 
     * @param message {@link String}
     * @return {@link String}
     */
    protected String messageSummary(OutlookMessage message) {
        String fromEmail = message.getFrom().getEmail();
        String fromName = message.getFrom().getDisplayName();
        Date time = message.getCreated().getTime();

        Locale locale = Locale.ENGLISH;
        ResourceBundle res = resourceBundleService.getResourceBundle("locale.outlook.Outlook", locale);

        DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL, locale);
        DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale);

        StringBuilder fromLine = new StringBuilder();
        fromLine.append(fromName);
        fromLine.append('<');
        fromLine.append(fromEmail);
        fromLine.append('>');

        StringBuilder summary = new StringBuilder();
        summary.append(res.getString("Outlook.activity.from"));
        summary.append(": <a href='mailto:");
        summary.append(fromEmail);
        summary.append("' target='_top'>");
        summary.append(ContentReader.simpleEscapeHtml(fromLine.toString()));
        summary.append("</a> ");
        summary.append(res.getString("Outlook.activity.on"));
        summary.append(' ');
        summary.append(dateFormat.format(time));
        summary.append(' ');
        summary.append(res.getString("Outlook.activity.at"));
        summary.append(' ');
        summary.append(timeFormat.format(time));

        return summary.toString();
    }

    /**
     * Try detect is it a HTML content in the string.
     * 
     * @param content {@link String}
     * @return boolean
     */
    protected boolean isHTML(String content) {
        // XXX well, it's not proper, but working in most of cases approach
        int istart = content.indexOf("<html");
        int iend = content.indexOf("</html>");
        if (istart >= 0 && iend > 0 && istart < iend) {
            return true;
        }

        istart = content.indexOf("<body");
        iend = content.indexOf("</body>");
        if (istart >= 0 && iend > 0 && istart < iend) {
            return true;
        }

        istart = content.indexOf("<div");
        iend = content.indexOf("</div>");
        if (istart >= 0 && iend > 0 && istart < iend) {
            return true;
        }

        // FYI it's how looks message after MS Word pre-peocessor in Outlook for Windows:
        // everything in tables (no html, body or divs)
        istart = content.indexOf("<table");
        iend = content.indexOf("</table>");
        if (istart >= 0 && iend > 0 && istart < iend) {
            return true;
        }

        istart = content.indexOf("<style");
        iend = content.indexOf("</style>");
        if (istart >= 0 && iend > 0 && istart < iend) {
            return true;
        }

        return false;
    }

    /**
     * Allow full rage of HTML text and structures.
     * 
     * @param content {@link String}
     * @return {@link String} sanitized content
     */
    protected String safeHtml(String content) {
        String safe = htmlPolicy.sanitize(content);
        safe = makeLinksOpenNewWindow(safe);
        return safe;
    }

    /**
     * Allow only plain text.
     * 
     * @param content {@link String}
     * @return {@link String} sanitized content (as plain text)
     */
    protected String safeText(String content) {
        String safe = textPolicy.sanitize(content);
        safe = makeLinksOpenNewWindow(safe);
        safe = StringEscapeUtils.unescapeHtml(safe);
        return safe;
    }

    /**
     * Allow only activity tags as described in
     * https://www.exoplatform.com/docs/PLF43/PLFUserGuide.GettingStarted.ActivitiesInActivityStream.HTMLTags.
     * html.
     * 
     * @param text {@link String}
     * @return {@link String} allowed content
     */
    protected String safeActivityMessage(String text) {
        String safe = activityPolicy.sanitize(text);
        safe = makeLinksOpenNewWindow(safe);
        safe = StringEscapeUtils.unescapeHtml(safe);
        return safe;
    }

    /**
     * Make links open new window.
     *
     * @param text the text
     * @return the string
     */
    protected String makeLinksOpenNewWindow(String text) {
        // Make all links target a new window
        // Replace in all links with target attribute to its _blank value
        Matcher m = linkWithTarget.matcher(text);
        StringBuilder sb = new StringBuilder();
        int pos = 0;
        while (m.find()) {
            if (linkNotLocal.matcher(m.group()).find()) {
                int start = m.start(1);
                int end = m.end(1);
                if (start >= 0 && end >= 0) {
                    sb.append(text.substring(pos, start));
                    sb.append("target=\"_blank\"");
                    pos = end;
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Cannot find link target group in " + m.group(1));
                    }
                }
            }
        }
        if (pos < text.length()) {
            sb.append(text.substring(pos));
        }
        text = sb.toString();

        // Add in all links without target attribute add it with _blank value
        m = linkWithoutTarget.matcher(text);
        sb = new StringBuilder();
        pos = 0;
        while (m.find()) {
            if (linkNotLocal.matcher(m.group()).find()) {
                int start = m.start(2);
                int end = m.end(2);
                if (start >= 0 && end >= 0) {
                    sb.append(text.substring(pos, start));
                    sb.append(" target=\"_blank\"");
                    sb.append(text.substring(start, end));
                    pos = end;
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Cannot find link end group in " + m.group(2));
                    }
                }
            }
        }
        if (pos < text.length()) {
            sb.append(text.substring(pos));
        }
        return sb.toString();
    }

    /**
     * Method adapted from eXo Chat's WikiService.setPermissionForReportAsWiki().
     * 
     * @param users list of {@link String}
     * @param page {@link Page}
     * @param parentPage {@link Page}
     */
    protected void setPermissionForWikiPage(List<String> users, Page page, Page parentPage) {
        Permission[] allPermissions = new Permission[] {
                new Permission(org.exoplatform.wiki.mow.api.PermissionType.VIEWPAGE, true),
                new Permission(org.exoplatform.wiki.mow.api.PermissionType.EDITPAGE, true), };
        List<PermissionEntry> permissions = parentPage.getPermissions();
        if (permissions != null) {
            // remove any permission
            int anyIndex = -1;
            for (int i = 0; i < permissions.size(); i++) {
                PermissionEntry any = permissions.get(i);
                if (WIKI_PERMISSION_ANY.equals(any.getId()))
                    anyIndex = i;
            }
            if (anyIndex > -1) {
                permissions.remove(anyIndex);
            }
            for (int i = 0; i < users.size(); i++) {
                String strUser = users.get(i).toString();
                PermissionEntry userPermission = new PermissionEntry(strUser, strUser, IDType.USER, allPermissions);
                permissions.add(userPermission);
            }
            page.setPermissions(permissions);
        }
    }

    /**
     * Method inspired by code of UIPostForm.
     * 
     * @param categoryId {@link String}
     * @param forumId {@link String}
     * @param creator {@link String}
     * @param title {@link String}
     * @param summary {@link String}
     * @param content {@link String}
     * @return {@link Topic} created topic
     * @throws Exception when error
     */
    protected Topic createForumTopic(String categoryId, String forumId, String creator, String title,
            String summary, String content) throws Exception {
        // save topic in ForumService
        org.exoplatform.forum.service.UserProfile userProfile = forumService.getUserSettingProfile(creator);
        if (checkForumHasAddTopic(userProfile, categoryId, forumId)) {
            Topic topic = new Topic();

            String message;
            if (isHTML(content)) {
                message = safeHtml(content);
            } else {
                message = content;
            }

            String safeTitle = safeText(title);
            // check if title not empty
            if (safeTitle.length() <= 0 || safeTitle.equals("null")) {
                if (summary != null) {
                    safeTitle = safeText(summary);
                } else {
                    safeTitle = safeText(message.substring(0, ForumUtils.MAXTITLE - 5) + "...");
                }
            }
            if (safeTitle.length() > ForumUtils.MAXTITLE) {
                // TODO throw an exception to user to ask for shorten title
                safeTitle = new StringBuilder(safeTitle.substring(0, ForumUtils.MAXTITLE - 3)).append("...")
                        .toString();
            }

            String checksms = TransformHTML.cleanHtmlCode(message,
                    new ArrayList<String>((new ExtendedBBCodeProvider()).getSupportedBBCodes()));
            checksms = checksms.replaceAll("&nbsp;", " ");
            int t = checksms.trim().length();
            if (t > 0 && !checksms.equals("null")) {
                // TODO in else block handle too short or offending texts
            }
            Date currentDate = CommonUtils.getGreenwichMeanTime().getTime();
            message = CommonUtils.encodeSpecialCharInSearchTerm(message);
            message = TransformHTML.fixAddBBcodeAction(message);
            // TODO do we need this when using safe HTML?
            // message = message.replaceAll("<script", "&lt;script").replaceAll("<link",
            // "&lt;link").replaceAll("</script>",
            // "&lt;/script>");
            // remove any meta tags explicitly existing in the content
            // message = message.replaceAll("<meta.*?>", "");
            // remove all embedded global styles
            // message = message.replaceAll("<style.*?>[.\\s\\w\\W]*?<\\/style>", "");

            boolean isOffend = false;
            ForumAdministration forumAdministration = forumService.getForumAdministration();
            String[] censoredKeyword = ForumUtils.getCensoredKeyword(forumAdministration.getCensoredKeyword());
            checksms = checksms.toLowerCase();
            for (String string : censoredKeyword) {
                if (checksms.indexOf(string.trim()) >= 0) {
                    isOffend = true;
                    break;
                }
                if (safeTitle.toLowerCase().indexOf(string.trim()) >= 0) {
                    isOffend = true;
                    break;
                }
            }

            boolean topicClosed = false; // uiForm.getUIForumCheckBoxInput(FIELD_TOPICSTATE_SELECTBOX).isChecked();
            boolean topicLocked = false; // uiForm.getUIForumCheckBoxInput(FIELD_TOPICSTATUS_SELECTBOX).isChecked();
            boolean sticky = false; // uiForm.getUIForumCheckBoxInput(FIELD_STICKY_CHECKBOX).isChecked();
            boolean moderatePost = true; // uiForm.getUIForumCheckBoxInput(FIELD_MODERATEPOST_CHECKBOX).isChecked();
            boolean whenNewPost = true; // uiForm.getUIForumCheckBoxInput(FIELD_NOTIFYWHENADDPOST_CHECKBOX).isChecked();

            // TODO permissions?
            // UIPermissionPanel permissionTab = uiForm.getChildById(PERMISSION_TAB);
            String canPost = ForumUtils.EMPTY_STR; // permissionTab.getOwnersByPermission(CANPOST);
            String canView = ForumUtils.EMPTY_STR; // permissionTab.getOwnersByPermission(CANVIEW);

            // set link
            // FYI this original Forum code will use current "outlook" portlet path to build the link
            // ForumUtils.createdForumLink(ForumUtils.TOPIC, topic.getId(), false)
            String link = BuildLinkUtils.buildLink(forumId, topic.getId(), PORTLET_INFO.FORUM);
            // finally escape the title
            safeTitle = CommonUtils.encodeSpecialCharInTitle(safeTitle);
            topic.setTopicName(safeTitle);
            topic.setModifiedBy(creator);
            topic.setModifiedDate(currentDate);

            if (summary != null) {
                // if summary given then we assume need quote the message content
                StringBuilder topicContent = new StringBuilder();
                if (summary != null) {
                    // HTML also contains message summary
                    topicContent.append(
                            "<div class='messageSummary' style='word-wrap: break-word; min-height: 30px;'>");
                    topicContent.append(summary);
                    topicContent.append("</div>");
                }
                topicContent.append(
                        "<div class='messageQuote' style='overflow:auto;'><div class='messageContent' style='position: relative; float: left;"
                                + "box-sizing: border-box; padding-left: 7px; min-width: 100%; max-height: 100%;"
                                + "border-width: 0px 0px 0px 12px; border-style: solid; border-color: #999999; background-color: white;'>");
                topicContent.append(message);
                topicContent.append("</div></div>");
                message = topicContent.toString();
            } // otherwise message content will be a content of the topic post

            topic.setDescription(message);
            topic.setLink(link);
            if (whenNewPost) {
                String email = userProfile.getEmail();
                if (email == null || email.length() <= 0) {
                    try {
                        email = organization.getUserHandler().findUserByName(creator).getEmail();
                    } catch (Exception e) {
                        email = "true";
                    }
                }
                topic.setIsNotifyWhenAddPost(email);
            } else {
                topic.setIsNotifyWhenAddPost(ForumUtils.EMPTY_STR);
            }
            topic.setIsWaiting(isOffend);
            topic.setIsClosed(topicClosed);
            topic.setIsLock(topicLocked);
            topic.setIsModeratePost(moderatePost);
            topic.setIsSticky(sticky);

            topic.setIcon("uiIconForumTopic uiIconForumLightGray");
            String[] canPosts = ForumUtils.splitForForum(canPost);
            String[] canViews = ForumUtils.splitForForum(canView);

            topic.setCanView(canViews);
            topic.setCanPost(canPosts);
            topic.setIsApproved(true); // !hasForumMod

            MessageBuilder messageBuilder = new MessageBuilder();
            WebuiRequestContext context = WebuiRequestContext.getCurrentInstance();
            ResourceBundle res = resourceBundleService.getResourceBundle("locale.portlet.forum.ForumPortlet",
                    context.getLocale());
            if (res != null) {
                // TODO this will not work as resources aren't reachable here - it's DEAD CODE in fact
                try {
                    messageBuilder.setContent(res.getString("UINotificationForm.label.notifyEmailContentDefault"));
                    String header = res.getString("UINotificationForm.label.notifyEmailHeaderSubjectDefault");
                    messageBuilder.setHeaderSubject(
                            header == null || header.trim().length() == 0 ? ForumUtils.EMPTY_STR : header);
                    messageBuilder.setTypes(res.getString("UIForumPortlet.label.category"),
                            res.getString("UIForumPortlet.label.forum"),
                            res.getString("UIForumPortlet.label.topic"),
                            res.getString("UIForumPortlet.label.post"));
                } catch (Exception e) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Failed to get resource bundle for Forum default content email notification", e);
                    }
                }
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(
                            "Locale resource bundle cannot be found for Forum default content email notification");
                }
            }

            messageBuilder.setLink(link);

            topic.setOwner(creator);
            topic.setCreatedDate(currentDate);
            topic.setLastPostBy(creator);
            topic.setLastPostDate(currentDate);
            topic.setVoteRating(0.0);
            topic.setUserVoteRating(new String[] {});
            try {
                String remoteAddr = ForumUtils.EMPTY_STR;
                remoteAddr = WebUIUtils.getRemoteIP();
                topic.setRemoteAddr(remoteAddr);
                forumService.saveTopic(categoryId, forumId, topic, true, false, messageBuilder);
                if (userProfile.getIsAutoWatchMyTopics()) {
                    List<String> values = new ArrayList<String>();
                    values.add(userProfile.getEmail());
                    String path = new StringBuilder(categoryId).append(ForumUtils.SLASH).append(forumId)
                            .append(ForumUtils.SLASH).append(topic.getId()).toString();
                    forumService.addWatch(1, path, values, creator);
                }
            } catch (PathNotFoundException e) {
                throw new OutlookException("Error saving forum topic '" + title + "'", e);
            }
            return topic;
        } else {
            throw new BadParameterException("Cannot add forum topic. Check user permissions or forum settings.");
        }
    }

    /**
     * Check forum has add topic.
     *
     * @param userProfile the user profile
     * @param categoryId the category id
     * @param forumId the forum id
     * @return true, if successful
     * @throws Exception the exception
     */
    protected boolean checkForumHasAddTopic(org.exoplatform.forum.service.UserProfile userProfile,
            String categoryId, String forumId) throws Exception {
        // FYI Adapted code from UIForumPortlet.

        // is guest or banned
        if (userProfile.getUserRole() == org.exoplatform.forum.service.UserProfile.GUEST
                || userProfile.getIsBanned() || userProfile.isDisabled()) {
            return false;
        }
        try {
            Category cate = forumService.getCategory(categoryId);
            Forum forum = forumService.getForum(categoryId, forumId);
            if (forum == null) {
                return false;
            }
            // forum close or lock
            if (forum.getIsClosed() || forum.getIsLock()) {
                return false;
            }
            // isAdmin
            if (userProfile.getUserRole() == 0) {
                return true;
            }
            // is moderator
            if (userProfile.getUserRole() == 1) {
                String[] morderators = ForumUtils.arraysMerge(cate.getModerators(), forum.getModerators());
                //
                if (ForumServiceUtils.isModerator(morderators, userProfile.getUserId())) {
                    return true;
                }
            }
            // FYI it's possible to ban IP of forum
            // check access category
            if (!ForumServiceUtils.hasPermission(cate.getUserPrivate(), userProfile.getUserId())) {
                return false;
            }
            // can add topic on category/forum
            String[] canCreadTopic = ForumUtils.arraysMerge(forum.getCreateTopicRole(), cate.getCreateTopicRole());
            if (!ForumServiceUtils.hasPermission(canCreadTopic, userProfile.getUserId())) {
                return false;
            }
        } catch (Exception e) {
            LOG.warn(String.format("Check permission to add topic of category %s, forum %s unsuccessfully.",
                    categoryId, forumId));
            if (LOG.isDebugEnabled()) {
                LOG.debug(e);
            }
            return false;
        }
        return true;
    }

    /**
     * Get the space name of node.
     *
     * @param node {@link Node}
     * @return {@link String} the group name
     * @throws RepositoryException when storage error
     */
    private static String getSpaceName(Node node) throws RepositoryException {
        NodeHierarchyCreator nodeHierarchyCreator = (NodeHierarchyCreator) ExoContainerContext.getCurrentContainer()
                .getComponentInstanceOfType(NodeHierarchyCreator.class);
        String groupPath = nodeHierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH);
        String spacesFolder = groupPath + "/spaces/";
        String spaceName = "";
        String nodePath = node.getPath();
        if (nodePath.startsWith(spacesFolder)) {
            spaceName = nodePath.substring(spacesFolder.length());
            spaceName = spaceName.substring(0, spaceName.indexOf("/"));
        }

        return spaceName;
    }

    /**
     * Make JCR compatible item name.
     * 
     * @param name {@link String}
     * @return {@link String} JCR compatible name of local file
     */
    public static String cleanName(String name) {
        String str = accentsConverter.transliterate(name.trim());
        // the character ? seems to not be changed to d by the transliterate function
        StringBuilder cleanedStr = new StringBuilder(str.trim());
        // delete special character
        if (cleanedStr.length() == 1) {
            char c = cleanedStr.charAt(0);
            if (c == '.' || c == '/' || c == ':' || c == '[' || c == ']' || c == '*' || c == '\'' || c == '"'
                    || c == '|') {
                // any -> _<NEXNUM OF c>
                cleanedStr.deleteCharAt(0);
                cleanedStr.append('_');
                cleanedStr.append(Integer.toHexString(c).toUpperCase());
            }
        } else {
            for (int i = 0; i < cleanedStr.length(); i++) {
                char c = cleanedStr.charAt(i);
                if (c == '/' || c == ':' || c == '[' || c == ']' || c == '*' || c == '\'' || c == '"' || c == '|') {
                    cleanedStr.deleteCharAt(i);
                    cleanedStr.insert(i, '_');
                } else if (!(Character.isLetterOrDigit(c) || Character.isWhitespace(c) || c == '.' || c == '-'
                        || c == '_')) {
                    cleanedStr.deleteCharAt(i--);
                }
            }
            // XXX finally ensure the name doesn't hava a dot at the end
            // https://github.com/exo-addons/outlook/issues/5
            // https://jira.exoplatform.org/browse/COMMONS-510
            int lastCharIndex = cleanedStr.length() - 1;
            char c = cleanedStr.charAt(lastCharIndex);
            if (c == '.') {
                cleanedStr.deleteCharAt(lastCharIndex);
            }
        }
        return cleanedStr.toString().trim(); // finally trim also
    }
}