org.alfresco.repo.imap.ImapServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.imap.ImapServiceImpl.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco 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 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.imap;

import static org.alfresco.repo.imap.AlfrescoImapConst.DICTIONARY_TEMPLATE_PREFIX;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.mail.Flags;
import javax.mail.Header;
import javax.mail.Flags.Flag;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.model.ImapModel;
import org.alfresco.repo.admin.SysAdminParams;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.imap.AlfrescoImapConst.ImapViewMode;
import org.alfresco.repo.imap.config.ImapConfigMountPointsBean;
import org.alfresco.repo.imap.exception.AlfrescoImapRuntimeException;
import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnRestoreNodePolicy;
import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.site.SiteDoesNotExistException;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.lock.NodeLockedException;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileFolderUtil;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.model.SubFolderFilter;
import org.alfresco.service.cmr.preference.PreferenceService;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.FileFilterMode;
import org.alfresco.util.FileFilterMode.Client;
import org.alfresco.util.GUID;
import org.alfresco.util.MaxSizeMap;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.config.RepositoryFolderConfigBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.extensions.surf.util.I18NUtil;

import com.icegreen.greenmail.store.FolderException;
import com.icegreen.greenmail.store.SimpleStoredMessage;

/**
 * @author Dmitry Vaserin
 * @author Arseny Kovalchuk
 * @author David Ward
 * @since 3.2
 */
public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCreateChildAssociationPolicy,
        OnDeleteChildAssociationPolicy, OnUpdatePropertiesPolicy, BeforeDeleteNodePolicy {
    private Log logger = LogFactory.getLog(ImapServiceImpl.class);

    private static final String ERROR_FOLDER_ALREADY_EXISTS = "imap.server.error.folder_already_exist";
    private static final String ERROR_MAILBOX_NAME_IS_MANDATORY = "imap.server.error.mailbox_name_is_mandatory";
    private static final String ERROR_CANNOT_GET_A_FOLDER = "imap.server.error.cannot_get_a_folder";
    private static final String ERROR_CANNOT_PARSE_DEFAULT_EMAIL = "imap.server.error.cannot_parse_default_email";

    private static final String CHECKED_NODES = "imap.flaggable.aspect.checked.list";
    private static final String FAVORITE_SITES = "imap.favorite.sites.list";
    private static final String UIDVALIDITY_TRANSACTION_LISTENER = "imap.uidvalidity.txn.listener";

    private SysAdminParams sysAdminParams;
    private FileFolderService fileFolderService;
    private NodeService nodeService;
    private PermissionService permissionService;
    private ServiceRegistry serviceRegistry;
    private BehaviourFilter policyBehaviourFilter;
    private NamespaceService namespaceService;
    private SearchService searchService;
    private AttachmentsExtractor attachmentsExtractor;

    // Note that this cache need not be cluster synchronized, as it is keyed by the cluster-safe 
    // change token.  Key is username, changeToken
    private Map<Pair<String, String>, FolderStatus> folderCache;
    private int folderCacheSize = 1000;
    private ReentrantReadWriteLock folderCacheLock = new ReentrantReadWriteLock();
    private SimpleCache<NodeRef, CacheItem> messageCache;
    private Map<String, ImapConfigMountPointsBean> imapConfigMountPoints;
    private Map<String, Integer> mountPointIds;
    private RepositoryFolderConfigBean[] ignoreExtractionFoldersBeans;
    private RepositoryFolderConfigBean imapHomeConfigBean;

    private NodeRef imapHomeNodeRef;
    private Set<NodeRef> ignoreExtractionFolders;

    private String defaultFromAddress;
    private String defaultToAddress;
    private String repositoryTemplatePath;
    private boolean extractAttachmentsEnabled = true;

    private Map<EmailBodyFormat, String> defaultBodyTemplates;

    private final static Map<QName, Flags.Flag> qNameToFlag;
    private final static Map<Flags.Flag, QName> flagToQname;

    private long imapServerShuffleMoveDeleteDelay = 5000L;
    private static final Timer deleteDelayTimer = new Timer();

    private boolean imapServerEnabled = false;

    private List<String> messageHeadersToPersist = Collections.<String>emptyList();

    static {
        qNameToFlag = new HashMap<QName, Flags.Flag>();
        qNameToFlag.put(ImapModel.PROP_FLAG_ANSWERED, Flags.Flag.ANSWERED);
        qNameToFlag.put(ImapModel.PROP_FLAG_DELETED, Flags.Flag.DELETED);
        qNameToFlag.put(ImapModel.PROP_FLAG_DRAFT, Flags.Flag.DRAFT);
        qNameToFlag.put(ImapModel.PROP_FLAG_SEEN, Flags.Flag.SEEN);
        qNameToFlag.put(ImapModel.PROP_FLAG_RECENT, Flags.Flag.RECENT);
        qNameToFlag.put(ImapModel.PROP_FLAG_FLAGGED, Flags.Flag.FLAGGED);

        flagToQname = new HashMap<Flags.Flag, QName>();
        flagToQname.put(Flags.Flag.ANSWERED, ImapModel.PROP_FLAG_ANSWERED);
        flagToQname.put(Flags.Flag.DELETED, ImapModel.PROP_FLAG_DELETED);
        flagToQname.put(Flags.Flag.DRAFT, ImapModel.PROP_FLAG_DRAFT);
        flagToQname.put(Flags.Flag.SEEN, ImapModel.PROP_FLAG_SEEN);
        flagToQname.put(Flags.Flag.RECENT, ImapModel.PROP_FLAG_RECENT);
        flagToQname.put(Flags.Flag.FLAGGED, ImapModel.PROP_FLAG_FLAGGED);
    }

    /**
     * Bootstrap initialization bean for the service implementation.
     * 
     * @author Derek Hulley
     * @since 3.2
     */
    public static class ImapServiceBootstrap extends AbstractLifecycleBean {
        private ImapServiceImpl service;

        public void setService(ImapServiceImpl service) {
            this.service = service;
        }

        @Override
        protected void onBootstrap(ApplicationEvent event) {
            service.startupInTxn(false);
        }

        @Override
        protected void onShutdown(ApplicationEvent event) {
            AuthenticationUtil.runAs(new RunAsWork<Void>() {
                @Override
                public Void doWork() throws Exception {
                    if (service.getImapServerEnabled()) {
                        service.shutdown();
                    }
                    return null;
                }
            }, AuthenticationUtil.getSystemUserName());
        }
    }

    public void setSysAdminParams(SysAdminParams sysAdminParams) {
        this.sysAdminParams = sysAdminParams;
    }

    public void setMessageCache(SimpleCache<NodeRef, CacheItem> messageCache) {
        this.messageCache = messageCache;
    }

    public void setFileFolderService(FileFolderService fileFolderService) {
        this.fileFolderService = fileFolderService;
    }

    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    public void setPermissionService(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
    }

    public void setPolicyFilter(BehaviourFilter policyFilter) {
        this.policyBehaviourFilter = policyFilter;
    }

    public void setAttachmentsExtractor(AttachmentsExtractor attachmentsExtractor) {
        this.attachmentsExtractor = attachmentsExtractor;
    }

    public void setImapHome(RepositoryFolderConfigBean imapHomeConfigBean) {
        this.imapHomeConfigBean = imapHomeConfigBean;
    }

    public void setFolderCacheSize(int folderCacheSize) {
        this.folderCacheSize = folderCacheSize;
    }

    public String getDefaultFromAddress() {
        return defaultFromAddress;
    }

    public void setDefaultFromAddress(String defaultFromAddress) {
        this.defaultFromAddress = defaultFromAddress;
    }

    public String getDefaultToAddress() {
        return defaultToAddress;
    }

    public void setDefaultToAddress(String defaultToAddress) {
        this.defaultToAddress = defaultToAddress;
    }

    public String getWebApplicationContextUrl() {
        return sysAdminParams.getAlfrescoProtocol() + "://" + sysAdminParams.getAlfrescoHost() + ":"
                + sysAdminParams.getAlfrescoPort() + "/" + sysAdminParams.getAlfrescoContext();
    }

    public String getShareApplicationContextUrl() {
        return sysAdminParams.getShareProtocol() + "://" + sysAdminParams.getShareHost() + ":"
                + sysAdminParams.getSharePort() + "/" + sysAdminParams.getShareContext();
    }

    public String getRepositoryTemplatePath() {
        return repositoryTemplatePath;
    }

    public void setRepositoryTemplatePath(String repositoryTemplatePath) {
        this.repositoryTemplatePath = repositoryTemplatePath;
    }

    public void setImapConfigMountPoints(ImapConfigMountPointsBean[] imapConfigMountPointsBeans) {
        this.imapConfigMountPoints = new LinkedHashMap<String, ImapConfigMountPointsBean>(
                imapConfigMountPointsBeans.length * 2);
        this.mountPointIds = new HashMap<String, Integer>(imapConfigMountPointsBeans.length * 2);
        for (int i = 0; i < imapConfigMountPointsBeans.length; i++) {
            String name = imapConfigMountPointsBeans[i].getMountPointName();
            this.imapConfigMountPoints.put(name, imapConfigMountPointsBeans[i]);
            this.mountPointIds.put(name, i + 1);
        }
    }

    public void setIgnoreExtractionFolders(final RepositoryFolderConfigBean[] ignoreExtractionFolders) {
        this.ignoreExtractionFoldersBeans = ignoreExtractionFolders;
    }

    public void setExtractAttachmentsEnabled(boolean extractAttachmentsEnabled) {
        this.extractAttachmentsEnabled = extractAttachmentsEnabled;
    }

    public void setImapServerEnabled(boolean enabled) {
        this.imapServerEnabled = enabled;
    }

    public void setMessageHeadersToPersist(List<String> headers) {
        this.messageHeadersToPersist = headers;
    }

    public void setImapServerShuffleMoveDeleteDelay(long imapServerShuffleMoveDeleteDelay) {
        this.imapServerShuffleMoveDeleteDelay = imapServerShuffleMoveDeleteDelay;
    }

    public boolean getImapServerEnabled() {
        return this.imapServerEnabled;
    }

    // ---------------------- Lifecycle Methods ------------------------------

    public void init() {
        PropertyCheck.mandatory(this, "imapConfigMountPoints", imapConfigMountPoints);
        PropertyCheck.mandatory(this, "ignoreExtractionFoldersBeans", ignoreExtractionFoldersBeans);
        PropertyCheck.mandatory(this, "imapHome", imapHomeConfigBean);

        PropertyCheck.mandatory(this, "fileFolderService", fileFolderService);
        PropertyCheck.mandatory(this, "nodeService", nodeService);
        PropertyCheck.mandatory(this, "permissionService", permissionService);
        PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry);
        PropertyCheck.mandatory(this, "defaultFromAddress", defaultFromAddress);
        PropertyCheck.mandatory(this, "defaultToAddress", defaultToAddress);
        PropertyCheck.mandatory(this, "repositoryTemplatePath", repositoryTemplatePath);
        PropertyCheck.mandatory(this, "policyBehaviourFilter", policyBehaviourFilter);
        PropertyCheck.mandatory(this, "namespaceService", namespaceService);
        PropertyCheck.mandatory(this, "searchService", getSearchService());
        this.folderCache = new MaxSizeMap<Pair<String, String>, FolderStatus>(folderCacheSize, false);

        // be sure that a default e-mail is correct
        try {
            InternetAddress.parse(defaultFromAddress);
        } catch (AddressException ex) {
            throw new AlfrescoRuntimeException(ERROR_CANNOT_PARSE_DEFAULT_EMAIL,
                    new Object[] { defaultFromAddress });
        }

        try {
            InternetAddress.parse(defaultToAddress);
        } catch (AddressException ex) {
            throw new AlfrescoRuntimeException(ERROR_CANNOT_PARSE_DEFAULT_EMAIL, new Object[] { defaultToAddress });
        }
    }

    /**
     * This method is run as System within a single transaction on startup.
     */
    public void startup() {
        bindBehaviour();

        // Get NodeRefs for folders to ignore
        this.ignoreExtractionFolders = new HashSet<NodeRef>(ignoreExtractionFoldersBeans.length * 2);

        for (RepositoryFolderConfigBean ignoreExtractionFoldersBean : ignoreExtractionFoldersBeans) {
            NodeRef nodeRef = ignoreExtractionFoldersBean.getFolderPath(namespaceService, nodeService,
                    searchService, fileFolderService);

            if (!ignoreExtractionFolders.add(nodeRef)) {
                // It was already in the set
                throw new AlfrescoRuntimeException("The folder extraction path has been referenced already: \n"
                        + "   Folder: " + ignoreExtractionFoldersBean);
            }
        }

        // Locate or create IMAP home
        imapHomeNodeRef = imapHomeConfigBean.getOrCreateFolderPath(namespaceService, nodeService, searchService,
                fileFolderService);
    }

    public void shutdown() {
    }

    protected void startupInTxn(boolean force) {
        if (force || getImapServerEnabled()) {
            AuthenticationUtil.runAs(new RunAsWork<Void>() {
                @Override
                public Void doWork() throws Exception {
                    List<AlfrescoImapFolder> mailboxes = serviceRegistry.getTransactionService()
                            .getRetryingTransactionHelper()
                            .doInTransaction(new RetryingTransactionCallback<List<AlfrescoImapFolder>>() {
                                @Override
                                public List<AlfrescoImapFolder> execute() throws Throwable {
                                    startup();

                                    List<AlfrescoImapFolder> result = new LinkedList<AlfrescoImapFolder>();

                                    // Hit the mount points and warm the caches for early failure
                                    for (String mountPointName : imapConfigMountPoints.keySet()) {
                                        result.addAll(
                                                listMailboxes(
                                                        new AlfrescoImapUser(null,
                                                                AuthenticationUtil.getSystemUserName(), null),
                                                        mountPointName + "*", false));
                                    }

                                    return result;
                                }
                            });

                    // Let each mailbox search trigger its own distinct transaction
                    for (AlfrescoImapFolder mailbox : mailboxes) {
                        mailbox.getUidNext();
                    }

                    return null;
                }
            }, AuthenticationUtil.getSystemUserName());
        }
    }

    protected void bindBehaviour() {
        if (logger.isDebugEnabled()) {
            logger.debug("[bindBeahaviour] Binding behaviours");
        }
        PolicyComponent policyComponent = (PolicyComponent) serviceRegistry
                .getService(QName.createQName(NamespaceService.ALFRESCO_URI, "policyComponent"));

        // Only listen to folders we've tagged with imap properties - not all folders or we'll really slow down the repository!
        policyComponent.bindAssociationBehaviour(OnCreateChildAssociationPolicy.QNAME, ImapModel.ASPECT_IMAP_FOLDER,
                ContentModel.ASSOC_CONTAINS,
                new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT));
        policyComponent.bindAssociationBehaviour(OnDeleteChildAssociationPolicy.QNAME, ImapModel.ASPECT_IMAP_FOLDER,
                ContentModel.ASSOC_CONTAINS,
                new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.EVERY_EVENT));
        policyComponent.bindClassBehaviour(OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_CONTENT,
                new JavaBehaviour(this, "onUpdateProperties", NotificationFrequency.EVERY_EVENT));
        policyComponent.bindClassBehaviour(BeforeDeleteNodePolicy.QNAME, ContentModel.TYPE_CONTENT,
                new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT));
        policyComponent.bindClassBehaviour(OnRestoreNodePolicy.QNAME, ContentModel.TYPE_CONTENT,
                new JavaBehaviour(this, "onRestoreNode", NotificationFrequency.EVERY_EVENT));
    }

    // ---------------------- Service Methods --------------------------------

    public SimpleStoredMessage getMessage(FileInfo mesInfo) throws MessagingException {
        NodeRef nodeRef = mesInfo.getNodeRef();
        Date modified = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
        if (modified != null) {
            CacheItem cached = messageCache.get(nodeRef);
            if (cached != null) {
                if (cached.getModified().equals(modified)) {
                    return cached.getMessage();
                }
            }
            SimpleStoredMessage message = createImapMessage(mesInfo, true);
            messageCache.put(nodeRef, new CacheItem(modified, message));
            return message;
        } else {
            SimpleStoredMessage message = createImapMessage(mesInfo, true);
            return message;
        }
    }

    public SimpleStoredMessage createImapMessage(FileInfo fileInfo, boolean generateBody)
            throws MessagingException {
        // TODO MER 26/11/2010- this test should really be that the content of the node is of type message/RFC822
        Long key = (Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID);
        if (nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) {
            return new SimpleStoredMessage(new ImapModelMessage(fileInfo, serviceRegistry, generateBody),
                    new Date(), key);
        } else {
            return new SimpleStoredMessage(new ContentModelMessage(fileInfo, serviceRegistry, generateBody),
                    new Date(), key);
        }
    }

    public void expungeMessage(FileInfo fileInfo) {
        Flags flags = getFlags(fileInfo);
        if (flags.contains(Flags.Flag.DELETED)) {
            // See MNT-12259
            //fileFolderService.delete(fileInfo.getNodeRef());
            hideAndDelete(fileInfo.getNodeRef());
            messageCache.remove(fileInfo.getNodeRef());
        }
    }

    /**
     * Workaround for MNT-12259
     * @param nodeRef NodeRef
     */
    @SuppressWarnings("deprecation")
    private void hideAndDelete(final NodeRef nodeRef) {
        FileFilterMode.setClient(FileFilterMode.Client.imap);
        fileFolderService.setHidden(nodeRef, true);
        {
            // Get the current user
            final String deleteDelayUser = AuthenticationUtil.getFullyAuthenticatedUser();
            // Add a timed task to really delete the file
            TimerTask deleteDelayTask = new TimerTask() {
                @Override
                public void run() {
                    RunAsWork<Void> deleteDelayRunAs = new RunAsWork<Void>() {
                        @Override
                        public Void doWork() throws Exception {
                            return serviceRegistry.getTransactionService().getRetryingTransactionHelper()
                                    .doInTransaction(new RetryingTransactionCallback<Void>() {
                                        @Override
                                        public Void execute() throws Throwable {
                                            // Ignore if it is NOT hidden: the shuffle may have finished; the operation may have failed
                                            if (!nodeService.exists(nodeRef)
                                                    || !fileFolderService.isHidden(nodeRef)) {
                                                return null;
                                            }

                                            // Since this will run in a different thread, the client thread-local must be set
                                            // or else unhiding the node will not unhide it for IMAP.
                                            FileFilterMode.setClient(FileFilterMode.Client.imap);

                                            // Unhide the node, e.g. for archiving
                                            fileFolderService.setHidden(nodeRef, false);

                                            // This is the transaction-aware service
                                            fileFolderService.delete(nodeRef);

                                            if (logger.isDebugEnabled()) {
                                                logger.debug("Node has been async deleted " + nodeRef);
                                            }

                                            return null;
                                        }
                                    });
                        }
                    };
                    try {
                        AuthenticationUtil.runAs(deleteDelayRunAs, deleteDelayUser);
                    } catch (Throwable e) {
                        // consume exception to avoid it leaking from the TimerTask and causing the Timer to
                        // no longer accept tasks to be scheduled.
                        logger.info("Exception thrown during IMAP delete timer task.", e);
                    }
                }
            };
            // Schedule a real delete 5 seconds after the current time
            if (logger.isDebugEnabled()) {
                logger.debug("Delete timer is scheduled for " + nodeRef);
            }
            deleteDelayTimer.schedule(deleteDelayTask, imapServerShuffleMoveDeleteDelay);
        }
    }

    /**
     * @throws AlfrescoImapRuntimeException
     */
    public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist,
            boolean mayCreate) {
        if (mailboxName == null) {
            throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY));
        }
        // A request for the hierarchy delimiter
        if (mailboxName.length() == 0) {
            return new AlfrescoImapFolder(user.getLogin(), this, serviceRegistry);
        }
        final NodeRef root;
        List<String> pathElements = null;
        ImapViewMode viewMode = ImapViewMode.ARCHIVE;
        int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
        int mountPointId = 0;

        String rootPath = (index > 0) ? (mailboxName.substring(0, index)) : (mailboxName);

        ImapConfigMountPointsBean imapConfigMountPoint = this.imapConfigMountPoints.get(rootPath);
        if (imapConfigMountPoint != null) {
            mountPointId = this.mountPointIds.get(rootPath);
            root = imapConfigMountPoint.getFolderPath(serviceRegistry.getNamespaceService(), nodeService,
                    searchService, fileFolderService);

            if (index > 0) {
                pathElements = Arrays.asList(mailboxName.substring(index + 1)
                        .split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)));
            }

            viewMode = imapConfigMountPoint.getMode();
        } else {
            root = getUserImapHomeRef(user.getLogin());
            pathElements = Arrays.asList(mailboxName.split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)));
        }

        FileInfo mailFolder;
        try {
            if (null != pathElements) {
                mailFolder = fileFolderService.resolveNamePath(root, pathElements, !mayCreate);
            } else {
                mailFolder = fileFolderService.getFileInfo(root);
            }
        } catch (FileNotFoundException e) {
            throw new AlfrescoImapRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] { mailboxName },
                    new FolderException(FolderException.NOT_LOCAL));
        }
        if (mailFolder == null) {
            if (!mayCreate) {
                throw new AlfrescoImapRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] { mailboxName },
                        new FolderException(FolderException.NOT_LOCAL));
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Creating mailbox: " + mailboxName);
            }
            mailFolder = FileFolderUtil.makeFolders(fileFolderService, root, pathElements,
                    ContentModel.TYPE_FOLDER);
        } else {
            if (!mayExist) {
                throw new AlfrescoImapRuntimeException(ERROR_FOLDER_ALREADY_EXISTS,
                        new FolderException(FolderException.ALREADY_EXISTS_LOCALLY));
            }
        }
        String path = (null != pathElements) ? (pathElements.get(pathElements.size() - 1)) : (rootPath);
        return new AlfrescoImapFolder(mailFolder, user.getLogin(), path, mailboxName, viewMode, this,
                serviceRegistry, true, isExtractionEnabled(mailFolder.getNodeRef()), mountPointId);
    }

    public void deleteMailbox(AlfrescoImapUser user, String mailboxName) {
        if (logger.isDebugEnabled()) {
            logger.debug("Deleting mailbox: mailboxName=" + mailboxName);
        }
        if (mailboxName == null) {
            throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY));
        }

        AlfrescoImapFolder folder = getOrCreateMailbox(user, mailboxName, true, false);
        NodeRef nodeRef = folder.getFolderInfo().getNodeRef();

        List<FileInfo> childFolders = fileFolderService.listFolders(nodeRef);

        if (childFolders.isEmpty()) {
            folder.signalDeletion();
            // Delete child folders and messages
            fileFolderService.delete(nodeRef);
        } else {
            if (folder.isSelectable()) {
                // Delete all messages for this folder
                // Don't delete subfolders and their messages
                List<FileInfo> messages = fileFolderService.listFiles(nodeRef);
                for (FileInfo message : messages) {
                    fileFolderService.delete(message.getNodeRef());
                }
                nodeService.addAspect(nodeRef, ImapModel.ASPECT_IMAP_FOLDER_NONSELECTABLE, null);
            } else {
                throw new AlfrescoRuntimeException(
                        mailboxName + " - Can't delete a non-selectable store with children.");
            }
        }
    }

    public void renameMailbox(AlfrescoImapUser user, String oldMailboxName, String newMailboxName) {
        if (oldMailboxName == null || newMailboxName == null) {
            throw new IllegalArgumentException(ERROR_MAILBOX_NAME_IS_MANDATORY);
        }

        AlfrescoImapFolder sourceNode = getOrCreateMailbox(user, oldMailboxName, true, false);

        if (logger.isDebugEnabled()) {
            logger.debug("Renaming folder oldMailboxName=" + oldMailboxName + " newMailboxName=" + newMailboxName);
        }

        NodeRef newMailParent;
        String newMailName;
        int index = newMailboxName.lastIndexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
        if (index < 0) {
            newMailParent = getUserImapHomeRef(user.getLogin());
            newMailName = newMailboxName;
        } else {
            newMailParent = getOrCreateMailbox(user, newMailboxName.substring(0, index), true, true).getFolderInfo()
                    .getNodeRef();
            newMailName = newMailboxName.substring(index + 1);
        }

        try {
            if (oldMailboxName.equalsIgnoreCase(AlfrescoImapConst.INBOX_NAME)) {
                // If you trying to rename INBOX
                // - just copy it to another folder with new name
                // and leave INBOX (with children) intact.
                fileFolderService.copy(sourceNode.getFolderInfo().getNodeRef(), newMailParent,
                        AlfrescoImapConst.INBOX_NAME);
            } else {
                fileFolderService.move(sourceNode.getFolderInfo().getNodeRef(), newMailParent, newMailName);
            }
        } catch (FileNotFoundException e) {
            throw new AlfrescoRuntimeException(e.getMessage(), e);
        } catch (FileExistsException e) {
            throw new AlfrescoRuntimeException(e.getMessage(), e);
        }
    }

    /**
     * Search for emails in specified folder depending on view mode.
     * 
     * Shallow list of files
     * 
     * @param contextNodeRef context folder for search
     * @param viewMode context folder view mode
     * @return list of emails that context folder contains.
     */
    public FolderStatus getFolderStatus(final String userName, final NodeRef contextNodeRef,
            ImapViewMode viewMode) {
        if (logger.isDebugEnabled()) {
            logger.debug("getFolderStatus contextNodeRef=" + contextNodeRef + ", viewMode=" + viewMode);
        }

        // No need to ACL check the change token read
        String changeToken = AuthenticationUtil.runAs(new RunAsWork<String>() {
            @Override
            public String doWork() throws Exception {
                return (String) nodeService.getProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN);
            }
        }, AuthenticationUtil.getSystemUserName());

        Pair<String, String> cacheKey = null;
        if (changeToken != null) {
            cacheKey = new Pair<String, String>(userName, changeToken);
            this.folderCacheLock.readLock().lock();
            try {
                FolderStatus result = this.folderCache.get(cacheKey);
                if (result != null) {
                    return result;
                }
            } finally {
                this.folderCacheLock.readLock().unlock();
            }
        }

        List<FileInfo> fileInfos = null;
        FileFilterMode.setClient(Client.imap);
        try {
            fileInfos = fileFolderService.listFiles(contextNodeRef);
        } finally {
            FileFilterMode.clearClient();
        }

        final NavigableMap<Long, FileInfo> currentSearch = new TreeMap<Long, FileInfo>();

        switch (viewMode) {
        case MIXED:
            for (FileInfo fileInfo : fileInfos) {
                currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo);
            }
            break;
        case ARCHIVE:
            for (FileInfo fileInfo : fileInfos) {
                if (nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) {
                    currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo);
                }
            }
            break;
        case VIRTUAL:
            for (FileInfo fileInfo : fileInfos) {
                if (!nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) {
                    currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo);
                }
            }
            break;
        }

        int messageCount = currentSearch.size(), recentCount = 0, unseenCount = 0, firstUnseen = 0;
        int i = 1;
        for (FileInfo fileInfo : currentSearch.values()) {
            Flags flags = getFlags(fileInfo);
            if (flags.contains(Flags.Flag.RECENT)) {
                recentCount++;
            }
            if (!flags.contains(Flags.Flag.SEEN)) {
                if (firstUnseen == 0) {
                    firstUnseen = i;
                }
                unseenCount++;
            }
            i++;
        }
        // Add the IMAP folder aspect with appropriate initial values if it is not already there
        if (changeToken == null) {
            changeToken = GUID.generate();
            cacheKey = new Pair<String, String>(userName, changeToken);
            final String finalToken = changeToken;
            doAsSystem(new RunAsWork<Void>() {
                @Override
                public Void doWork() throws Exception {
                    nodeService.setProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN, finalToken);
                    nodeService.setProperty(contextNodeRef, ImapModel.PROP_MAXUID,
                            currentSearch.isEmpty() ? 0 : currentSearch.lastKey());
                    return null;
                }
            });
        }
        Long uidValidity = (Long) nodeService.getProperty(contextNodeRef, ImapModel.PROP_UIDVALIDITY);
        FolderStatus result = new FolderStatus(messageCount, recentCount, firstUnseen, unseenCount,
                uidValidity == null ? 0 : uidValidity, changeToken, currentSearch);
        this.folderCacheLock.writeLock().lock();
        try {
            FolderStatus oldResult = this.folderCache.get(cacheKey);
            if (oldResult != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("At end of getFolderStatus. Found info in cache, changeToken:" + changeToken);
                }

                return oldResult;
            }
            this.folderCache.put(cacheKey, result);

            if (logger.isDebugEnabled()) {
                logger.debug("At end of getFolderStatus. Found files:" + currentSearch.size() + ", changeToken:"
                        + changeToken);
            }
            return result;
        } finally {
            this.folderCacheLock.writeLock().unlock();
        }
    }

    public void subscribe(AlfrescoImapUser user, String mailbox) {
        if (logger.isDebugEnabled()) {
            logger.debug("Subscribing: " + user + ", " + mailbox);
        }
        AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false);
        PersonService personService = serviceRegistry.getPersonService();
        NodeRef userRef = personService.getPerson(user.getLogin());

        nodeService.removeAssociation(userRef, mailFolder.getFolderInfo().getNodeRef(),
                ImapModel.ASSOC_IMAP_UNSUBSCRIBED);
    }

    public void unsubscribe(AlfrescoImapUser user, String mailbox) {
        if (logger.isDebugEnabled()) {
            logger.debug("Unsubscribing: " + user + ", " + mailbox);
        }
        AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false);
        if (mailFolder.getFolderInfo() != null) {
            PersonService personService = serviceRegistry.getPersonService();
            NodeRef userRef = personService.getPerson(user.getLogin());
            nodeService.createAssociation(userRef, mailFolder.getFolderInfo().getNodeRef(),
                    ImapModel.ASSOC_IMAP_UNSUBSCRIBED);
        } else {
            // perhaps the folder has been deleted by another async process?
            logger.debug("Unable to find folder to unsubscribe");
        }
    }

    /**
     * Return flags that belong to the specified imap folder.
     * 
     * @param messageInfo imap folder info.
     * @return flags.
     */
    public Flags getFlags(FileInfo messageInfo) {
        Flags flags = new Flags();
        Map<QName, Serializable> props = nodeService.getProperties(messageInfo.getNodeRef());

        for (QName key : qNameToFlag.keySet()) {
            Boolean value = (Boolean) props.get(key);
            if (value != null && value) {
                flags.add(qNameToFlag.get(key));
            }
        }

        return flags;
    }

    /**
     * Set flags to the specified imapFolder.
     * 
     * @param messageInfo FileInfo of imap Folder.
     * @param flags flags to set.
     * @param value value to set.
     */
    public void setFlags(FileInfo messageInfo, Flags flags, boolean value) {
        checkForFlaggableAspect(messageInfo.getNodeRef());

        for (Flags.Flag flag : flags.getSystemFlags()) {
            setFlag(messageInfo, flag, value);
        }
    }

    /**
     * Set flags to the specified message.
     * 
     * @param messageInfo FileInfo of imap Folder
     * @param flag flag to set.
     * @param value value value to set.
     */
    public void setFlag(FileInfo messageInfo, Flag flag, boolean value) {
        setFlag(messageInfo.getNodeRef(), flag, value);
    }

    private void setFlag(NodeRef nodeRef, Flag flag, boolean value) {
        String permission = (flag == Flag.DELETED ? PermissionService.DELETE_NODE
                : PermissionService.WRITE_PROPERTIES);

        AccessStatus status = permissionService.hasPermission(nodeRef, permission);
        if (status == AccessStatus.DENIED) {
            if (flag == Flag.DELETED) {
                logger.debug("[setFlag] Access denied to set DELETED FLAG:" + nodeRef);
                throw new AccessDeniedException("No permission to set DELETED flag");
            }
            if (flag == Flag.SEEN) {
                logger.debug("[setFlag] Access denied to set SEEN FLAG:" + nodeRef);
                //TODO - should we throw an exception here?
                //throw new AccessDeniedException("No permission to set DELETED flag");
            } else {

                logger.debug("[setFlag] Access denied to set flag:" + nodeRef);
                throw new AccessDeniedException("No permission to set flag:" + flag.toString());
            }
        } else {
            checkForFlaggableAspect(nodeRef);
            policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
            policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
            try {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                            "set flag nodeRef:" + nodeRef + ",flag:" + flagToQname.get(flag) + ", value:" + value);
                }
                nodeService.setProperty(nodeRef, flagToQname.get(flag), value);
                messageCache.remove(nodeRef);
            } finally {
                policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
                policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
            }
        }
    }

    /**
     * Depend on listSubscribed param, list Mailboxes or list subscribed Mailboxes
     */
    public List<AlfrescoImapFolder> listMailboxes(AlfrescoImapUser user, String mailboxPattern,
            boolean listSubscribed) {
        if (logger.isDebugEnabled()) {
            logger.debug("[listMailboxes] user:" + user.getLogin() + ", mailboxPattern:" + mailboxPattern
                    + ", listSubscribed:" + listSubscribed);
        }
        List<AlfrescoImapFolder> result = new LinkedList<AlfrescoImapFolder>();

        // List mailboxes that are in mount points
        int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
        String rootPath = index == -1 ? mailboxPattern : mailboxPattern.substring(0, index);
        boolean found = false;

        String userName = user.getLogin();
        Set<NodeRef> unsubscribedFodlers = getUnsubscribedFolders(userName);

        for (String mountPointName : imapConfigMountPoints.keySet()) {
            if (mountPointName.matches(rootPath.replaceAll("[%\\*]", ".*"))) {
                NodeRef mountPoint = getMountPoint(mountPointName);
                if (mountPoint != null) {
                    int mountPointId = mountPointIds.get(mountPointName);
                    FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint);
                    ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode();
                    if (index < 0) {

                        if (!listSubscribed || !unsubscribedFodlers.contains(mountPointFileInfo.getNodeRef())) {
                            result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName,
                                    mountPointName, viewMode, isExtractionEnabled(mountPointFileInfo.getNodeRef()),
                                    this, serviceRegistry, mountPointId));
                        } else if (rootPath.endsWith("%") && !expandFolder(mountPoint, user, mountPointName, "%",
                                true, viewMode, mountPointId).isEmpty()) // \NoSelect 
                        {
                            result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName,
                                    mountPointName, viewMode, this, serviceRegistry, false,
                                    isExtractionEnabled(mountPointFileInfo.getNodeRef()), mountPointId));
                        }
                        if (rootPath.endsWith("*")) {
                            result.addAll(expandFolder(mountPoint, user, mountPointName, "*", listSubscribed,
                                    viewMode, mountPointId));
                        }
                    } else {
                        result.addAll(expandFolder(mountPoint, user, mountPointName,
                                mailboxPattern.substring(index + 1), listSubscribed, viewMode, mountPointId));
                    }
                }
                // If we had an exact match, there is no point continuing to search
                if (mountPointName.equals(rootPath)) {
                    found = true;
                    break;
                }
            }
        }

        // List mailboxes that are in user IMAP Home
        if (!found) {
            NodeRef root = getUserImapHomeRef(user.getLogin());
            result.addAll(expandFolder(root, user, "", mailboxPattern, listSubscribed, ImapViewMode.ARCHIVE, 0));
        }

        logger.debug("listMailboxes returning size:" + result.size());

        return result;
    }

    /**
     * Recursively search the given root to get a list of folders
     * 
     * @return List<AlfrescoImapFolder>
     */
    private List<AlfrescoImapFolder> expandFolder(NodeRef root, AlfrescoImapUser user, String rootPath,
            String mailboxPattern, boolean listSubscribed, ImapViewMode viewMode, int mountPointId) {
        if (logger.isDebugEnabled()) {
            logger.debug("expand folder: root:" + root + " user: " + user + " :mailboxPattern=" + mailboxPattern);
        }
        if (mailboxPattern == null)
            return null;
        int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);

        String name = null;
        if (index < 0) {
            name = mailboxPattern;
        } else {
            name = mailboxPattern.substring(0, index);
        }
        String rootPathPrefix = rootPath.length() == 0 ? "" : rootPath + AlfrescoImapConst.HIERARCHY_DELIMITER;

        if (logger.isDebugEnabled()) {
            logger.debug("Listing mailboxes: name=" + name);
        }

        List<AlfrescoImapFolder> fullList = new LinkedList<AlfrescoImapFolder>();
        ImapSubFolderFilter filter = new ImapSubFolderFilter(viewMode, name.replace('%', '*'));
        List<FileInfo> list;
        // Only list this folder if we have a wildcard name. Otherwise do a direct lookup by name.
        if (name.contains("*") || name.contains("%")) {
            FileFilterMode.setClient(Client.imap);
            try {
                // Check if the mount point is site root
                // then shortcut to user's favorite sites
                // MNT-12055
                if (serviceRegistry.getSiteService().getSiteRoot().equals(root)) {
                    list = fileFolderService.toFileInfoList(filter.favs);
                } else {
                    list = fileFolderService.listFolders(root);
                }
            } finally {
                FileFilterMode.clearClient();
            }
        } else {
            NodeRef nodeRef = fileFolderService.searchSimple(root, name);
            FileInfo fileInfo;
            list = nodeRef == null || !(fileInfo = fileFolderService.getFileInfo(nodeRef)).isFolder()
                    ? Collections.<FileInfo>emptyList()
                    : Collections.singletonList(fileInfo);
        }

        if (index < 0) {
            String userName = user.getLogin();
            Set<NodeRef> unsubscribedFodlers = getUnsubscribedFolders(userName);

            // This is the last level            
            for (FileInfo fileInfo : list) {
                if (!filter.isEnterSubfolder(fileInfo.getNodeRef())) {
                    continue;
                }
                String folderPath = rootPathPrefix + fileInfo.getName();
                if (!listSubscribed || !unsubscribedFodlers.contains(fileInfo.getNodeRef())) {
                    fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath,
                            viewMode, isExtractionEnabled(fileInfo.getNodeRef()), this, serviceRegistry,
                            mountPointId));
                } else if (name.endsWith("%")
                        && !expandFolder(fileInfo.getNodeRef(), user, folderPath, "%", true, viewMode, mountPointId)
                                .isEmpty()) // \NoSelect 
                {
                    fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath,
                            viewMode, this, serviceRegistry, false, isExtractionEnabled(fileInfo.getNodeRef()),
                            mountPointId));
                }
                if (name.endsWith("*")) {
                    fullList.addAll(expandFolder(fileInfo.getNodeRef(), user, folderPath, "*", listSubscribed,
                            viewMode, mountPointId));
                }
            }
        } else {
            // If (index != -1) this is not the last level
            for (FileInfo folder : list) {
                if (!filter.isEnterSubfolder(folder.getNodeRef())) {
                    continue;
                }
                fullList.addAll(expandFolder(folder.getNodeRef(), user, rootPathPrefix + folder.getName(),
                        mailboxPattern.substring(index + 1), listSubscribed, viewMode, mountPointId));
            }
        }
        return fullList;
    }

    /**
     * Map of mount points. Name of mount point == key in the map.
     * 
     * @return Map of mount points.
     */
    private NodeRef getMountPoint(String rootFolder) {
        final ImapConfigMountPointsBean config = imapConfigMountPoints.get(rootFolder);
        try {
            // Get node reference. Do it in new transaction to avoid RollBack in case when AccessDeniedException is thrown.
            return serviceRegistry.getTransactionService().getRetryingTransactionHelper()
                    .doInTransaction(new RetryingTransactionCallback<NodeRef>() {
                        public NodeRef execute() throws Exception {
                            try {
                                return config.getFolderPathOrNull(namespaceService, nodeService, searchService,
                                        fileFolderService);
                            } catch (AccessDeniedException e) {
                                if (logger.isDebugEnabled()) {
                                    logger.debug("A mount point is skipped due to Access Dennied. \n"
                                            + "   Mount point: " + config + "\n" + "   User: "
                                            + AuthenticationUtil.getFullyAuthenticatedUser());
                                }
                            }

                            return null;
                        }
                    }, true, true);
        } catch (AccessDeniedException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("A mount point is skipped due to Access Dennied. \n" + "   Mount point: " + config
                        + "\n" + "   User: " + AuthenticationUtil.getFullyAuthenticatedUser());
            }
        }
        return null;
    }

    /**
     * Get the node ref of the user's imap home.   Will create it on demand if it 
     * does not already exist.
     * 
     * @param userName user name
     * @return user IMAP home reference and create it if it doesn't exist.
     */
    public NodeRef getUserImapHomeRef(final String userName) {
        NodeRef userHome = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<NodeRef>() {
            public NodeRef doWork() throws Exception {
                // Look for user imap home
                NodeRef userHome = fileFolderService.searchSimple(imapHomeNodeRef, userName);
                if (userHome == null) {
                    // user imap home does not exist
                    NodeRef result = fileFolderService.create(imapHomeNodeRef, userName, ContentModel.TYPE_FOLDER)
                            .getNodeRef();
                    nodeService.setProperty(result, ContentModel.PROP_DESCRIPTION, userName);

                    // create user inbox
                    fileFolderService.create(result, AlfrescoImapConst.INBOX_NAME, ContentModel.TYPE_FOLDER);

                    // Set permissions on user's imap home
                    permissionService.setInheritParentPermissions(result, false);
                    permissionService.setPermission(result, PermissionService.OWNER_AUTHORITY,
                            PermissionService.ALL_PERMISSIONS, true);

                    return result;
                }

                return userHome;
            }
        }, AuthenticationUtil.getSystemUserName());

        return userHome;
    }

    private Set<NodeRef> getUnsubscribedFolders(String userName) {
        Set<NodeRef> result = new HashSet<NodeRef>();
        PersonService personService = serviceRegistry.getPersonService();
        NodeRef userRef = personService.getPerson(userName);
        List<AssociationRef> unsubscribedFodlers = nodeService.getTargetAssocs(userRef,
                ImapModel.ASSOC_IMAP_UNSUBSCRIBED);
        for (AssociationRef asocRef : unsubscribedFodlers) {
            result.add(asocRef.getTargetRef());
        }

        return result;
    }

    private String getCurrentUser() {
        return AuthenticationUtil.getFullyAuthenticatedUser();
    }

    /**
     * Return list of "favourite" sites, that belong to the specified user and are marked as "Imap favourite"
     * 
     * @param userName name of user
     * @return List of favourite sites.
     */
    private List<NodeRef> getFavouriteSites(final String userName) {
        if (logger.isDebugEnabled()) {
            logger.debug("[getFavouriteSites] entry for user: " + userName);
        }
        List<NodeRef> favSites = AlfrescoTransactionSupport.getResource(FAVORITE_SITES);
        if (logger.isDebugEnabled()) {
            if (favSites == null) {
                logger.debug("[getFavouriteSites] There is no Favorite sites' list bound to transaction "
                        + AlfrescoTransactionSupport.getTransactionId());
            } else {
                logger.debug("[getFavouriteSites] Found Favorite sites' list bound to transaction "
                        + AlfrescoTransactionSupport.getTransactionId());
            }
        }
        if (favSites == null) {
            favSites = new LinkedList<NodeRef>();

            PreferenceService preferenceService = (PreferenceService) serviceRegistry
                    .getService(ServiceRegistry.PREFERENCE_SERVICE);
            Map<String, Serializable> prefs = preferenceService.getPreferences(userName,
                    AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES);

            for (String key : prefs.keySet()) {
                Boolean isImapFavourite = (Boolean) prefs.get(key);
                if (isImapFavourite != null && isImapFavourite) {
                    String siteName = key.substring(AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES.length() + 1); // count the dot
                    boolean isMember = false;
                    try {
                        isMember = serviceRegistry.getSiteService().isMember(siteName, userName);
                    } catch (SiteDoesNotExistException sdne) {
                        // Ignore, see MNT-13579
                        // The site might be archived. In this case it will still be in user's preferences.
                    }

                    if (isMember) {
                        SiteInfo siteInfo = serviceRegistry.getSiteService().getSite(siteName);
                        if (siteInfo != null) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("[getFavouriteSites] User: " + userName + " Favourite site: "
                                        + siteInfo.getShortName());
                            }
                            favSites.add(siteInfo.getNodeRef());
                        }
                    }
                }
            }
            if (logger.isDebugEnabled()) {
                logger.debug("[getFavouriteSites] Bind new Favorite sites' list to transaction "
                        + AlfrescoTransactionSupport.getTransactionId());
            }
            AlfrescoTransactionSupport.bindResource(FAVORITE_SITES, favSites);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("[getFavouriteSites] end for user: " + userName);
        }

        return favSites;
    }

    /**
     * Checks for the existence of the flaggable aspect and adds it if it is not already present on the folder. 
     * @param nodeRef NodeRef
     */
    private void checkForFlaggableAspect(NodeRef nodeRef) {
        Set<NodeRef> alreadyChecked = AlfrescoTransactionSupport.getResource(CHECKED_NODES);
        if (alreadyChecked == null) {
            alreadyChecked = new HashSet<NodeRef>();
        }
        if (alreadyChecked.contains(nodeRef)) {
            if (logger.isDebugEnabled()) {
                logger.debug("[checkForFlaggableAspect] Flaggable aspect has been already checked for {" + nodeRef
                        + "}");
            }
            return;
        }
        try {
            serviceRegistry.getLockService().checkForLock(nodeRef);
        } catch (NodeLockedException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("[checkForFlaggableAspect] Node {" + nodeRef + "} is locked");
            }
            alreadyChecked.add(nodeRef);
            return;
        }
        if (!nodeService.hasAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE)) {
            AccessStatus status = permissionService.hasPermission(nodeRef, PermissionService.WRITE_PROPERTIES);
            if (status == AccessStatus.DENIED) {
                logger.debug("[checkForFlaggableAspect] No permissions to add FLAGGABLE aspect" + nodeRef);
            } else {
                try {
                    policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
                    policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
                    logger.debug("[checkForFlaggableAspect] Adding flaggable aspect to nodeRef: " + nodeRef);
                    Map<QName, Serializable> aspectProperties = new HashMap<QName, Serializable>();
                    nodeService.addAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE, aspectProperties);
                } finally {
                    policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
                    policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
                }
            }
        }
        alreadyChecked.add(nodeRef);
        AlfrescoTransactionSupport.bindResource(CHECKED_NODES, alreadyChecked);
    }

    private boolean isExtractionEnabled(NodeRef nodeRef) {
        return extractAttachmentsEnabled && !ignoreExtractionFolders.contains(nodeRef);
    }

    public String getDefaultEmailBodyTemplate(EmailBodyFormat type) {
        if (defaultBodyTemplates == null) {
            defaultBodyTemplates = new HashMap<EmailBodyFormat, String>(4);

            for (EmailBodyFormat onetype : EmailBodyFormat.values()) {
                String result = onetype.getClasspathTemplatePath();
                try {
                    // This query uses cm:name to find the template node(s).
                    // For the case where the templates are renamed, it would be better to use a QName path-based query.

                    final StringBuilder templateName = new StringBuilder(DICTIONARY_TEMPLATE_PREFIX).append("_")
                            .append(onetype.getTypeSubtype()).append("_").append(onetype.getWebApp())
                            .append(".ftl");

                    final String repositoryTemplatePath = getRepositoryTemplatePath();
                    int indexOfStoreDelim = repositoryTemplatePath.indexOf(StoreRef.URI_FILLER);
                    if (indexOfStoreDelim == -1) {
                        throw new IllegalArgumentException(
                                "Bad path format, " + StoreRef.URI_FILLER + " not found");
                    }
                    indexOfStoreDelim += StoreRef.URI_FILLER.length();
                    int indexOfPathDelim = repositoryTemplatePath.indexOf("/", indexOfStoreDelim);
                    if (indexOfPathDelim == -1) {
                        throw new IllegalArgumentException("Bad path format, '/' not found");
                    }
                    final String storePath = repositoryTemplatePath.substring(0, indexOfPathDelim);
                    final String rootPathInStore = repositoryTemplatePath.substring(indexOfPathDelim);
                    final String query = rootPathInStore + "/" + NamespaceService.CONTENT_MODEL_PREFIX + ":"
                            + templateName;
                    if (logger.isDebugEnabled()) {
                        logger.debug("[getDefaultEmailBodyTemplate] Query: " + query);
                    }
                    StoreRef storeRef = new StoreRef(storePath);

                    NodeRef rootNode = nodeService.getRootNode(storeRef);

                    List<NodeRef> templates = searchService.selectNodes(rootNode, query, null, namespaceService,
                            true);
                    if (templates == null || templates.size() == 0) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("template not found:" + templateName);
                        }
                        throw new AlfrescoRuntimeException(String.format(
                                "[getDefaultEmailBodyTemplate] IMAP message template '%1$s' does not exist in the path '%2$s'.",
                                templateName, repositoryTemplatePath));
                    }
                    final NodeRef defaultLocaleTemplate = templates.get(0);

                    NodeRef localisedSibling = serviceRegistry.getFileFolderService()
                            .getLocalizedSibling(defaultLocaleTemplate);
                    result = localisedSibling.toString();
                }
                // We are catching all exceptions. E.g. search service can possibly throw an exceptions on malformed queries.
                catch (Exception e) {
                    logger.error("ImapServiceImpl [getDefaultEmailBodyTemplate]", e);
                }
                defaultBodyTemplates.put(onetype, result);
            }
        }
        return defaultBodyTemplates.get(type);
    }

    /**
     * This method should returns a unique identifier of Alfresco server. The possible UID may be calculated based on IP address, Server port, MAC address, Web Application context.
     * This UID should be parseable into initial components. This necessary for the implementation of the following case: If the message being copied (e.g. drag-and-drop) between
     * two different Alfresco accounts in the IMAP client, we must unambiguously identify from which Alfresco server this message being copied. The message itself does not contain
     * content data, so we must download it from the initial server (e.g. using download content servlet) and save it into destination repository.
     * 
     * @return String representation of unique identifier of Alfresco server
     */
    public String getAlfrescoServerUID() {
        // TODO Implement as javadoc says.
        return "Not-Implemented";
    }

    /**
     * Share Site Exclusion Filter
     */
    private class ImapSubFolderFilter implements SubFolderFilter {
        /**
         * Exclude Share Sites of TYPE_SITE
         */
        private Collection<QName> typesToExclude;
        private List<NodeRef> favs;
        private String mailboxPattern;
        private ImapViewMode imapViewMode;

        ImapSubFolderFilter(ImapViewMode imapViewMode) {
            this.imapViewMode = imapViewMode;
            this.typesToExclude = ImapServiceImpl.this.serviceRegistry.getDictionaryService()
                    .getSubTypes(SiteModel.TYPE_SITE, true);
            this.favs = getFavouriteSites(getCurrentUser());
        }

        ImapSubFolderFilter(ImapViewMode imapViewMode, String mailboxPattern) {
            this(imapViewMode);
            this.mailboxPattern = mailboxPattern.replaceAll("\\*", "(.)*");
            ;
        }

        @Override
        public boolean isEnterSubfolder(ChildAssociationRef subfolderRef) {
            return isEnterSubfolder(subfolderRef.getChildRef());
        }

        public boolean isEnterSubfolder(NodeRef folder) {
            String name = (String) nodeService.getProperty(folder, ContentModel.PROP_NAME);
            if (mailboxPattern != null) {
                logger.debug("Child name: " + name);
                if (logger.isDebugEnabled()) {
                    logger.debug("Folder name: " + name + ". Pattern: " + mailboxPattern + ". Matches: "
                            + name.matches(mailboxPattern));
                }
                if (!name.matches(mailboxPattern))
                    return false;
            }
            QName typeOfFolder = nodeService.getType(folder);
            if (typesToExclude.contains(typeOfFolder)) {
                if (imapViewMode == ImapViewMode.VIRTUAL || imapViewMode == ImapViewMode.MIXED) {
                    /**
                     * In VIRTUAL and MIXED MODE WE SHOULD ONLY DISPLAY FOLDERS FROM FAVOURITE SITES
                     */
                    if (favs.contains(folder)) {
                        logger.debug("[ImapSubFolderFilter] (VIRTUAL) including fav site folder :" + name);
                        return true;
                    } else {
                        logger.debug("[ImapSubFolderFilter] (VIRTUAL) excluding non fav site folder :" + name);
                        return false;
                    }
                } else {
                    /**
                     * IN ARCHIVE MODE we don't display folders for any SITES, regardless of whether they are favourites.
                     */
                    logger.debug("[ImapSubFolderFilter] (ARCHIVE) excluding site folder :" + name);
                    return false;
                }
            }
            return true;
        }

    }

    private UidValidityTransactionListener getUidValidityTransactionListener(NodeRef folderRef) {
        String key = UIDVALIDITY_TRANSACTION_LISTENER + folderRef.toString();
        UidValidityTransactionListener txnListener = AlfrescoTransactionSupport.getResource(key);
        if (txnListener == null) {
            txnListener = new UidValidityTransactionListener(folderRef);
            AlfrescoTransactionSupport.bindListener(txnListener);
            AlfrescoTransactionSupport.bindResource(key, txnListener);
        }
        return txnListener;
    }

    @Override
    public void onCreateChildAssociation(final ChildAssociationRef childAssocRef, boolean isNewNode) {
        doAsSystem(new RunAsWork<Void>() {
            @Override
            public Void doWork() throws Exception {
                NodeRef childNodeRef = childAssocRef.getChildRef();

                if (serviceRegistry.getDictionaryService().isSubClass(nodeService.getType(childNodeRef),
                        ContentModel.TYPE_CONTENT)) {
                    long newId = (Long) nodeService.getProperty(childNodeRef, ContentModel.PROP_NODE_DBID);
                    // Keep a record of minimum and maximum node IDs in this folder in this transaction and add a listener that will
                    // update the UIDVALIDITY and MAXUID properties appropriately. Also force generation of a new change token
                    getUidValidityTransactionListener(childAssocRef.getParentRef()).recordNewUid(newId);
                    // Flag new content as recent
                    setFlag(childNodeRef, Flags.Flag.RECENT, true);
                }

                if (logger.isDebugEnabled()) {
                    logger.debug("[onCreateChildAssociation] Association " + childAssocRef
                            + " created. CHANGETOKEN will be changed.");
                }
                return null;
            }
        });
    }

    @Override
    public void onDeleteChildAssociation(final ChildAssociationRef childAssocRef) {
        doAsSystem(new RunAsWork<Void>() {
            @Override
            public Void doWork() throws Exception {
                NodeRef childNodeRef = childAssocRef.getChildRef();
                if (serviceRegistry.getDictionaryService().isSubClass(nodeService.getType(childNodeRef),
                        ContentModel.TYPE_CONTENT)) {
                    // Force generation of a new change token
                    getUidValidityTransactionListener(childAssocRef.getParentRef());

                    // Remove the message from the cache
                    messageCache.remove(childNodeRef);
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("[onDeleteChildAssociation] Association " + childAssocRef
                            + " created. CHANGETOKEN will be changed.");
                }
                return null;
            }
        });
    }

    @Override
    public void onUpdateProperties(final NodeRef nodeRef, final Map<QName, Serializable> before,
            final Map<QName, Serializable> after) {
        doAsSystem(new RunAsWork<Void>() {
            @Override
            public Void doWork() throws Exception {
                /**
                 * Imap only cares about a few properties however if those properties 
                 * change then the uidvalidity needs to be reset otherwise the new content
                 * won't get re-loaded.   This is nonsense for an email server, but needed for 
                 * modifiable repository.  Also we need to ignore certain properties.
                 */
                boolean hasChanged = false;

                if (!hasChanged) {
                    hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_NAME),
                            after.get(ContentModel.PROP_NAME));
                }
                if (!hasChanged) {
                    hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_AUTHOR),
                            after.get(ContentModel.PROP_AUTHOR));
                }
                if (!hasChanged) {
                    hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_TITLE),
                            after.get(ContentModel.PROP_TITLE));
                }
                if (!hasChanged) {
                    hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_DESCRIPTION),
                            after.get(ContentModel.PROP_DESCRIPTION));
                }

                if (!hasChanged) {
                    Serializable s1 = before.get(ContentModel.PROP_CONTENT);
                    Serializable s2 = after.get(ContentModel.PROP_CONTENT);

                    if (s1 != null && s2 != null) {
                        ContentData c1 = (ContentData) s1;
                        ContentData c2 = (ContentData) s2;

                        hasChanged = !EqualsHelper.nullSafeEquals(c1.getContentUrl(), c2.getContentUrl());
                    }
                }

                for (ChildAssociationRef parentAssoc : nodeService.getParentAssocs(nodeRef)) {
                    NodeRef folderRef = parentAssoc.getParentRef();
                    if (nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER)) {
                        messageCache.remove(nodeRef);

                        // Force generation of a new change token for the parent folders
                        UidValidityTransactionListener listener = getUidValidityTransactionListener(folderRef);

                        // if we have a significant change then we need to force a new uidvalidity.
                        if (hasChanged) {
                            logger.debug("message has changed - force new uidvalidity for the parent folder");
                            listener.forceNewUidvalidity();
                        }
                    }
                }
                return null;
            }
        });
    }

    @Override
    public void onRestoreNode(final ChildAssociationRef childAssocRef) {
        doAsSystem(new RunAsWork<Void>() {
            @Override
            public Void doWork() throws Exception {
                NodeRef childNodeRef = childAssocRef.getChildRef();
                if (serviceRegistry.getDictionaryService().isSubClass(nodeService.getType(childNodeRef),
                        ContentModel.TYPE_CONTENT)) {
                    setFlag(childNodeRef, Flags.Flag.DELETED, false);
                    setFlag(childNodeRef, Flags.Flag.SEEN, false);
                }

                NodeRef folderRef = childAssocRef.getParentRef();
                long newId = (Long) nodeService.getProperty(childNodeRef, ContentModel.PROP_NODE_DBID);
                if (nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER)) {
                    // Force generation of a new change token and updating the UIDVALIDITY 
                    getUidValidityTransactionListener(folderRef).recordNewUid(newId);
                }
                return null;
            }
        });
    }

    @Override
    public void beforeDeleteNode(final NodeRef nodeRef) {
        doAsSystem(new RunAsWork<Void>() {
            @Override
            public Void doWork() throws Exception {
                for (ChildAssociationRef parentAssoc : nodeService.getParentAssocs(nodeRef)) {
                    NodeRef folderRef = parentAssoc.getParentRef();
                    if (nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER)) {
                        messageCache.remove(nodeRef);

                        // Force generation of a new change token
                        getUidValidityTransactionListener(folderRef);
                    }
                }
                return null;
            }
        });
    }

    private <R> R doAsSystem(RunAsWork<R> work) {
        policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
        policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
        try {
            return AuthenticationUtil.runAs(work, AuthenticationUtil.getSystemUserName());
        } finally {
            policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
            policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
        }
    }

    private class UidValidityTransactionListener extends TransactionListenerAdapter {
        // Generate a unique token for each folder change with which we can validate session caches
        private String changeToken = GUID.generate();
        private NodeRef folderNodeRef;
        private Long minUid;
        private Long maxUid;
        private boolean forceNewUidValidity = false;

        public UidValidityTransactionListener(NodeRef folderNodeRef) {
            this.folderNodeRef = folderNodeRef;
        }

        public void forceNewUidvalidity() {
            this.forceNewUidValidity = true;
        }

        public void recordNewUid(long newUid) {
            if (this.minUid == null) {
                this.minUid = this.maxUid = newUid;
            } else if (newUid < this.minUid) {
                this.minUid = newUid;
            } else if (newUid > this.maxUid) {
                this.maxUid = newUid;
            }
        }

        @Override
        public void beforeCommit(boolean readOnly) {
            if (readOnly) {
                return;
            }

            doAsSystem(new RunAsWork<Void>() {
                @Override
                public Void doWork() throws Exception {
                    // This fires at the end of the transaction, so double-check that the node is still present
                    if (!nodeService.exists(folderNodeRef)) {
                        return null;
                    }

                    if (UidValidityTransactionListener.this.forceNewUidValidity
                            || UidValidityTransactionListener.this.minUid != null) {
                        long modifDate = System.currentTimeMillis();
                        Long oldMax = (Long) nodeService.getProperty(folderNodeRef, ImapModel.PROP_MAXUID);
                        // Only update UIDVALIDITY if a new node has and ID that is smaller or equals the old maximum (as UIDs are always meant to increase)
                        if (UidValidityTransactionListener.this.forceNewUidValidity || oldMax == null
                                || UidValidityTransactionListener.this.minUid <= oldMax) {
                            nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate);
                            if (logger.isDebugEnabled()) {
                                logger.debug("UIDVALIDITY was modified for folder, nodeRef:" + folderNodeRef);
                            }
                        }
                        if (UidValidityTransactionListener.this.maxUid != null) {
                            nodeService.setProperty(folderNodeRef, ImapModel.PROP_MAXUID,
                                    UidValidityTransactionListener.this.maxUid);
                            if (logger.isDebugEnabled()) {
                                logger.debug("MAXUID was modified for folder, nodeRef:" + folderNodeRef);
                            }
                        }
                    }
                    nodeService.setProperty(folderNodeRef, ImapModel.PROP_CHANGE_TOKEN, changeToken);
                    return null;
                }
            });
        }
    }

    public NodeRef getNodeSiteContainer(final NodeRef inputNodeRef) {
        return doAsSystem(new RunAsWork<NodeRef>() {
            @Override
            public NodeRef doWork() throws Exception {
                NodeRef nodeRef = inputNodeRef;
                while (true) {
                    NodeRef parent = nodeService.getPrimaryParent(nodeRef).getParentRef();
                    if (parent == null) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("The node with nodeRef:" + inputNodeRef + " is not in the site.");
                        }
                        nodeRef = null;
                        break;
                    }
                    nodeRef = parent;
                    if (nodeService.hasAspect(parent, SiteModel.ASPECT_SITE_CONTAINER)) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("The node with nodeRef:" + inputNodeRef + " is in the site.");
                        }
                        // found the container
                        break;
                    }
                }
                return nodeRef;
            }
        });

    }

    public String getContentFolderUrl(NodeRef contentNodeRef) {
        String url = "";
        String CONTAINER_URL_TEMPLATE = "%s/page/site/%s";
        String REPOSITORY_URL_TEMPLATE = "%s/page/%s";

        NodeRef siteContainerNodeRef = getNodeSiteContainer(contentNodeRef);

        if (siteContainerNodeRef != null) {
            // the node is in site's container
            // determine which one

            NodeRef siteNodeRef = nodeService.getPrimaryParent(siteContainerNodeRef).getParentRef();
            String siteName = ((String) nodeService.getProperty(siteNodeRef, ContentModel.PROP_NAME)).toLowerCase();
            String componentId = (String) nodeService.getProperty(siteContainerNodeRef,
                    SiteModel.PROP_COMPONENT_ID);
            switch (componentId.toLowerCase()) {
            case "datalists":
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(),
                        siteName + "/data-lists");
                break;
            case "wiki":
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(), siteName + "/wiki");
                break;
            case "links":
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(), siteName + "/links");
                break;
            case "calendar":
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(),
                        siteName + "/calendar");
                break;
            case "discussions":
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(),
                        siteName + "/discussions-topiclist");
                break;
            case "blog":
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(),
                        siteName + "/blog-postlist");
                break;
            case "documentlibrary":
                String pathFromSites = getPathFromSites(
                        nodeService.getPrimaryParent(contentNodeRef).getParentRef());
                StringBuilder parsedPath = new StringBuilder();
                String[] pathParts = pathFromSites.split("/");
                if (pathParts.length > 2) {
                    parsedPath.append(pathParts[0] + "/" + pathParts[1]);
                    parsedPath.append("?filter=path|");
                    for (int i = 2; i < pathParts.length; i++) {
                        parsedPath.append("/").append(pathParts[i]);
                    }
                } else {
                    parsedPath.append(pathFromSites);
                }
                url = String.format(CONTAINER_URL_TEMPLATE, getShareApplicationContextUrl(), parsedPath.toString());
                break;
            default:
                break;
            }
        } else {
            // the node is in repository
            String pathFromRepo = getPathFromRepo(nodeService.getPrimaryParent(contentNodeRef));
            StringBuilder parsedPath = new StringBuilder();
            String[] pathParts = pathFromRepo.split("/");
            if (pathParts.length > 1) {
                parsedPath.append(pathParts[0]);
                parsedPath.append("?filter=path|");
                for (int i = 1; i < pathParts.length; i++) {
                    parsedPath.append("/").append(pathParts[i]);
                }
            } else {
                parsedPath.append(pathFromRepo);
            }
            url = String.format(REPOSITORY_URL_TEMPLATE, getShareApplicationContextUrl(), parsedPath.toString());
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Resolved content folder URL:" + url + " for node " + contentNodeRef);
        }
        return url;
    }

    public void setNamespaceService(NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    public NamespaceService getNamespaceService() {
        return namespaceService;
    }

    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    public SearchService getSearchService() {
        return searchService;
    }

    public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage)
            throws IOException, MessagingException {
        attachmentsExtractor.extractAttachments(messageRef, originalMessage);
    }

    public String generateUniqueFilename(NodeRef destFolderNodeRef, String fileName) {
        if (fileFolderService.searchSimple(destFolderNodeRef, fileName) != null) {
            String name = fileName;
            String ext = "";
            if (fileName.lastIndexOf(".") != -1) {
                int index = fileName.lastIndexOf(".");
                name = fileName.substring(0, index);
                ext = fileName.substring(index);
            }
            int copyNum = 0;
            do {
                copyNum++;
            } while (fileFolderService.searchSimple(destFolderNodeRef, name + " (" + copyNum + ")" + ext) != null);
            fileName = name + " (" + copyNum + ")" + ext;
        }

        return fileName;
    }

    @SuppressWarnings("unchecked")
    public void persistMessageHeaders(NodeRef messageRef, MimeMessage message) {
        try {
            Enumeration<Header> headers = message.getAllHeaders();
            List<String> messaheHeadersProperties = new ArrayList<String>();
            while (headers.hasMoreElements()) {
                Header header = headers.nextElement();
                if (isPersistableHeader(header)) {
                    messaheHeadersProperties.add(
                            header.getName() + ImapModel.MESSAGE_HEADER_TO_PERSIST_SPLITTER + header.getValue());

                    if (logger.isDebugEnabled()) {
                        logger.debug("[persistHeaders] Persisting Header " + header.getName() + " : "
                                + header.getValue());
                    }
                }
            }

            Map<QName, Serializable> props = new HashMap<QName, Serializable>();
            props.put(ImapModel.PROP_MESSAGE_HEADERS, (Serializable) messaheHeadersProperties);

            serviceRegistry.getNodeService().addAspect(messageRef, ImapModel.ASPECT_IMAP_MESSAGE_HEADERS, props);
        } catch (MessagingException me) {

        }
    }

    private boolean isPersistableHeader(Header header) {
        for (String headerToPersist : messageHeadersToPersist) {
            if (headerToPersist.equalsIgnoreCase(header.getName())) {
                return true;
            }
        }
        return false;
    }

    static class CacheItem {
        private Date modified;
        private SimpleStoredMessage message;

        public CacheItem(Date modified, SimpleStoredMessage message) {
            this.setMessage(message);
            this.setModified(modified);
        }

        public void setModified(Date modified) {
            this.modified = modified;
        }

        public Date getModified() {
            return modified;
        }

        public void setMessage(SimpleStoredMessage message) {
            this.message = message;
        }

        public SimpleStoredMessage getMessage() {
            return message;
        }
    }

    @Override
    public String getPathFromSites(final NodeRef ref) {
        return doAsSystem(new RunAsWork<String>() {
            @Override
            public String doWork() throws Exception {
                String name = ((String) nodeService.getProperty(ref, ContentModel.PROP_NAME)).toLowerCase();
                if (nodeService.getType(ref).equals(SiteModel.TYPE_SITE)) {
                    return name;
                } else {
                    NodeRef parent = nodeService.getPrimaryParent(ref).getParentRef();
                    return getPathFromSites(parent) + "/" + name;
                }
            }
        });
    }

    @Override
    public String getPathFromRepo(final ChildAssociationRef assocRef) {
        return doAsSystem(new RunAsWork<String>() {
            @Override
            public String doWork() throws Exception {
                NodeRef ref = assocRef.getParentRef();
                String name = ((String) nodeService.getProperty(ref, ContentModel.PROP_NAME)).toLowerCase();
                ChildAssociationRef parent = nodeService.getPrimaryParent(ref);
                QName qname = parent.getQName();
                if (qname.equals(QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "company_home"))) {
                    return "repository";
                } else {
                    return getPathFromRepo(parent) + "/" + name;
                }
            }
        });
    }
}