/*
 * 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 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;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.http.entity.ContentType;
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 com.ibm.icu.text.Transliterator;

import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.commons.utils.ListAccess;
import org.exoplatform.commons.utils.StringCommonUtils;
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.DocumentService;
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.core.ManageableRepository;
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.NodeLocation;
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.manager.RelationshipManager;
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.plugin.doc.UIDocActivity;
import org.exoplatform.social.webui.activity.UIDefaultActivity;
import org.exoplatform.wcm.ext.component.activity.FileUIActivity;
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;

/**
 * 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;

    /** The social relationship manager. */
    protected final RelationshipManager socialRelationshipManager;

    /**
     * Instantiates a new user by given email, display name and username.
     *
     * @param email the email, can be <code>null</code>
     * @param displayName the display name, can be <code>null</code>
     * @param userName the user name in eXo organization
     * @throws OutlookException if userName is not valid
     */
    protected UserImpl(String email, String displayName, String userName) throws OutlookException {
      super(email, displayName, userName);
      this.socialIdentityManager = socialIdentityManager();
      this.socialActivityManager = socialActivityManager();
      this.socialRelationshipManager = socialRelationshipManager();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ExoSocialActivity postActivity(OutlookMessage message) throws OutlookException {
      // save text to user documents
      try {
        Node userDocs = userDocumentsNode(localUser);
        if (userDocs != null) {
          // Since upgrade to Platform 5.2 root of user folder has no user
          // permissions even for read.
          // Node userPublicFolder = userDocs.getParent().getNode("Public");
          Node sysPublicFolder = systemNode(userDocs.getSession().getWorkspace().getName(), userDocs.getPath() + "/Public");
          Node sysMessagesFolder = messagesFolder(sysPublicFolder, localUser, "member:/platform/users");
          Node sysMessageFile = addMessageFile(sysMessagesFolder, message);
          setPermissions(sysMessageFile, localUser, "member:/platform/users");
          sysMessagesFolder.save();
          // Take node under user session to use in the message and activity
          Node messageFile = node(sysMessageFile.getSession().getWorkspace().getName(), sysMessageFile.getPath());
          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());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Identity getSocialIdentity() throws Exception {
      return socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, getLocalUser(), true);
    }
  }

  /**
   * 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 resource document service. */
  protected final DocumentService                             documentService;

  /** 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);

  /**
   * 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,
                            DocumentService documentService)
      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;
    this.documentService = documentService;

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

  /**
   * {@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;
        }

        // new user instance
        UserImpl user = new UserImpl(email, displayName, exoUsername);
        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() {
    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);
  }

  /**
   * Social relationship manager.
   *
   * @return the relationship manager
   */
  protected RelationshipManager socialRelationshipManager() {
    return (RelationshipManager) ExoContainerContext.getCurrentContainer().getComponentInstanceOfType(RelationshipManager.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_NAME)) {
      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 {

    final String origType = org.exoplatform.wcm.ext.component.activity.listener.Utils.getActivityType();
    try {
      String author = user.getLocalUser();
      IdentityManager identityManager = socialIdentityManager();
      Identity authorIdentity = identityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, author, true);

      // FYI Code inspired by UIDocActivityComposer
      Map<String, String> activityParams = new LinkedHashMap<String, String>();
      Calendar activityDate = Calendar.getInstance();
      DateFormat dateFormatter = new SimpleDateFormat(ISO8601.SIMPLE_DATETIME_FORMAT);
      String fileName = "", id = "", doclink = "", docpath = "", mimeType = "", contenLink = "", workspace = "", repository = "",
          dateString = "", owner = "", isSymlink = "";
      String REGEX = "|@|";
      int i = 0;
      for (File f : files) {
        if (files.size() - 1 == i) {
          REGEX = "";
        }
        fileName += f.getName() + REGEX;
        id += f.getNode().getUUID() + REGEX;
        mimeType += f.getNode().getNode("jcr:content").getProperty("jcr:mimeType").getString() + REGEX;
        docpath += f.getNode().getPath() + REGEX;
        owner += author + REGEX;
        isSymlink += false + REGEX;
        try {
          contenLink += documentService.getLinkInDocumentsApp(NodeLocation.getNodeLocationByNode(f.getNode()).getPath()) + REGEX;
          doclink += org.exoplatform.wcm.webui.Utils.getWebdavURL(f.getNode()) + REGEX;
        } catch (Exception e) {
          LOG.error("Error getting node download link " + f.getNode(), e);
        }
        dateString += dateFormatter.format(activityDate.getTime()) + REGEX;
        workspace += destFolder.getNode().getSession().getWorkspace().getName() + REGEX;
        repository += ((ManageableRepository) destFolder.getNode().getSession().getRepository()).getConfiguration().getName()
            + REGEX;
        i++;
      }
      activityParams.put(OutlookAttachmentActivity.FILES, fileName);
      activityParams.put(OutlookAttachmentActivity.DOCTITLE, fileName);
      activityParams.put(OutlookAttachmentActivity.WORKSPACE, workspace);
      activityParams.put(OutlookAttachmentActivity.REPOSITORY, repository);
      activityParams.put(OutlookAttachmentActivity.COMMENT, comment);
      activityParams.put(OutlookAttachmentActivity.DOCLINK, doclink);
      activityParams.put(OutlookAttachmentActivity.DOCNAME, fileName);
      activityParams.put(OutlookAttachmentActivity.DOCPATH, docpath);
      activityParams.put(OutlookAttachmentActivity.PATH, docpath);
      activityParams.put(OutlookAttachmentActivity.AUTHOR, owner);
      activityParams.put(OutlookAttachmentActivity.DATE_CREATED, dateString);
      activityParams.put(OutlookAttachmentActivity.DATE_LAST_MODIFIED, dateString);
      activityParams.put(FileUIActivity.ID, id);
      activityParams.put(FileUIActivity.CONTENT_NAME, fileName);
      activityParams.put(FileUIActivity.ACTIVITY_STATUS, comment);
      activityParams.put(FileUIActivity.MIME_TYPE, mimeType);
      activityParams.put(FileUIActivity.CONTENT_LINK, contenLink);
      activityParams.put(FileUIActivity.DOCUMENT_TITLE, fileName);
      activityParams.put(UIDocActivity.IS_SYMLINK, isSymlink);

      // 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));

      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;
    } finally {
      org.exoplatform.wcm.ext.component.activity.listener.Utils.setActivityType(origType);
    }
  }

  /**
   * 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 = StringCommonUtils.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 = StringCommonUtils.encodeSpecialCharForSimpleInput(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;
      while (lastCharIndex >= 0 && (c = cleanedStr.charAt(lastCharIndex)) == '.') {
        cleanedStr.deleteCharAt(lastCharIndex);
        if (lastCharIndex == 0) {
          cleanedStr.append('_');
          cleanedStr.append(Integer.toHexString(c).toUpperCase());
        }
        lastCharIndex--;
      }
    }
    return cleanedStr.toString().trim(); // finally trim also
  }
}
