/* * Copyright (C) 2005-2010 Alfresco Software Limited. * * This file is part of Alfresco * * 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 . */ package org.alfresco.repo.imap; import static org.alfresco.repo.imap.AlfrescoImapConst.DICTIONARY_TEMPLATE_PREFIX; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.Date; 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.Set; import javax.mail.Flags; import javax.mail.Flags.Flag; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Part; import javax.mail.internet.ContentType; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeUtility; 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.node.NodeServicePolicies.BeforeDeleteNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy; import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.policy.Behaviour.NotificationFrequency; 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.SiteModel; import org.alfresco.repo.site.SiteServiceException; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.lock.NodeLockedException; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.SubFolderFilter; import org.alfresco.service.cmr.preference.PreferenceService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.MimetypeService; 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.ResultSet; 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.site.SiteInfo; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.PropertyCheck; import org.alfresco.util.Utf7; import org.alfresco.util.config.RepositoryFolderConfigBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.poi.hmef.HMEFMessage; import org.springframework.context.ApplicationEvent; import org.springframework.extensions.surf.util.AbstractLifecycleBean; import org.springframework.extensions.surf.util.I18NUtil; import org.springframework.util.FileCopyUtils; import com.icegreen.greenmail.imap.ImapConstants; /** * @author Dmitry Vaserin * @author Arseny Kovalchuk * @since 3.2 */ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPolicy, OnDeleteChildAssociationPolicy { private Log logger = LogFactory.getLog(ImapServiceImpl.class); private static final String ERROR_PERMISSION_DENIED = "imap.server.error.permission_denied"; 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_LISTENER_ALREADY_BOUND = "imap.uidvalidity.already.bound"; private SysAdminParams sysAdminParams; private FileFolderService fileFolderService; private NodeService nodeService; private PermissionService permissionService; private ServiceRegistry serviceRegistry; private BehaviourFilter policyBehaviourFilter; private MimetypeService mimetypeService; /** * Folders cache * * Key : folder name, Object : AlfrescoImapFolder */ private SimpleCache foldersCache; private Map imapConfigMountPoints; private RepositoryFolderConfigBean[] ignoreExtractionFoldersBeans; private RepositoryFolderConfigBean imapHomeConfigBean; private NodeRef imapHomeNodeRef; private Set ignoreExtractionFolders; private String defaultFromAddress; private String defaultToAddress; private String repositoryTemplatePath; private boolean extractAttachmentsEnabled = true; private Map defaultBodyTemplates; private final static Map qNameToFlag; private final static Map flagToQname; private boolean imapServerEnabled = false; static { qNameToFlag = new HashMap(); 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(); 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) { if (service.getImapServerEnabled()) { service.startup(); } } @Override protected void onShutdown(ApplicationEvent event) { if (service.getImapServerEnabled()) { service.shutdown(); } } } public void setSysAdminParams(SysAdminParams sysAdminParams) { this.sysAdminParams = sysAdminParams; } public void setFoldersCache(SimpleCache foldersCache) { this.foldersCache = foldersCache; } public SimpleCache getFoldersCache() { return foldersCache; } public FileFolderService getFileFolderService() { return fileFolderService; } public void setFileFolderService(FileFolderService fileFolderService) { this.fileFolderService = fileFolderService; } public void setMimetypeService(MimetypeService mimetypeService) { this.mimetypeService = mimetypeService; } 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 setImapHome(RepositoryFolderConfigBean imapHomeConfigBean) { this.imapHomeConfigBean = imapHomeConfigBean; } 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(imapConfigMountPointsBeans.length * 2); for (ImapConfigMountPointsBean bean : imapConfigMountPointsBeans) { this.imapConfigMountPoints.put(bean.getMountPointName(), bean); } } 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 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, "mimetypeService", mimetypeService); // 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}); } } public void startup() { bindBeahaviour(); final NamespaceService namespaceService = serviceRegistry.getNamespaceService(); final SearchService searchService = serviceRegistry.getSearchService(); // Hit the mount points for early failure AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { public Void doWork() throws Exception { getMountPoints(); return null; } }, AuthenticationUtil.getSystemUserName()); // Get NodeRefs for folders to ignore this.ignoreExtractionFolders = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() { public Set doWork() throws Exception { Set result = new HashSet(ignoreExtractionFoldersBeans.length * 2); for (RepositoryFolderConfigBean ignoreExtractionFoldersBean : ignoreExtractionFoldersBeans) { NodeRef nodeRef = ignoreExtractionFoldersBean.getFolderPath( namespaceService, nodeService, searchService, fileFolderService); if (!result.add(nodeRef)) { // It was already in the set throw new AlfrescoRuntimeException( "The folder extraction path has been referenced already: \n" + " Folder: " + ignoreExtractionFoldersBean); } } return result; } }, AuthenticationUtil.getSystemUserName()); // Locate or create IMAP home imapHomeNodeRef = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { public NodeRef doWork() throws Exception { return imapHomeConfigBean.getOrCreateFolderPath(namespaceService, nodeService, searchService, fileFolderService); } }, AuthenticationUtil.getSystemUserName()); } public void shutdown() { } protected void bindBeahaviour() { if (logger.isDebugEnabled()) { logger.debug("[bindBeahaviour] Binding behaviours"); } PolicyComponent policyComponent = (PolicyComponent) serviceRegistry.getService(QName.createQName(NamespaceService.ALFRESCO_URI, "policyComponent")); policyComponent.bindAssociationBehaviour( OnCreateChildAssociationPolicy.QNAME, ContentModel.TYPE_FOLDER, ContentModel.ASSOC_CONTAINS, new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); policyComponent.bindAssociationBehaviour( OnCreateChildAssociationPolicy.QNAME, ContentModel.TYPE_CONTENT, ContentModel.ASSOC_CONTAINS, new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); policyComponent.bindAssociationBehaviour( OnDeleteChildAssociationPolicy.QNAME, ContentModel.TYPE_FOLDER, ContentModel.ASSOC_CONTAINS, new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); policyComponent.bindAssociationBehaviour( OnDeleteChildAssociationPolicy.QNAME, ContentModel.TYPE_CONTENT, ContentModel.ASSOC_CONTAINS, new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); policyComponent.bindClassBehaviour( BeforeDeleteNodePolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); policyComponent.bindClassBehaviour( BeforeDeleteNodePolicy.QNAME, ContentModel.TYPE_FOLDER, new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); } // ---------------------- Service Methods -------------------------------- public List listSubscribedMailboxes(AlfrescoImapUser user, String mailboxPattern) { mailboxPattern = Utf7.decode(mailboxPattern, Utf7.UTF7_MODIFIED); if (logger.isDebugEnabled()) { logger.debug("Listing subscribed mailboxes: mailboxPattern=" + mailboxPattern); } mailboxPattern = getMailPathInRepo(mailboxPattern); if (logger.isDebugEnabled()) { logger.debug("Listing subscribed mailboxes: mailbox path in Alfresco=" + mailboxPattern); } return listMailboxes(user, mailboxPattern, true); } public List listMailboxes(AlfrescoImapUser user, String mailboxPattern) { mailboxPattern = Utf7.decode(mailboxPattern, Utf7.UTF7_MODIFIED); if (logger.isDebugEnabled()) { logger.debug("Listing mailboxes: mailboxPattern=" + mailboxPattern); } mailboxPattern = getMailPathInRepo(mailboxPattern); if (logger.isDebugEnabled()) { logger.debug("Listing mailboxes: mailbox path in Alfresco Repository = " + mailboxPattern); } return listMailboxes(user, mailboxPattern, false); } public AlfrescoImapFolder createMailbox(AlfrescoImapUser user, String mailboxName) { if (mailboxName == null) { throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY)); } mailboxName = Utf7.decode(mailboxName, Utf7.UTF7_MODIFIED); if (logger.isDebugEnabled()) { logger.debug("Creating mailbox: " + mailboxName); } NodeRef root = getMailboxRootRef(mailboxName, user.getLogin()); NodeRef parentNodeRef = root; // it is used for hierarhy deep search. for (String folderName : getMailPathInRepo(mailboxName).split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER))) { NodeRef child = fileFolderService.searchSimple(parentNodeRef, folderName); if (logger.isDebugEnabled()) { logger.debug("Trying to create folder '" + folderName + "'"); } if (child == null) { // folder doesn't exist AccessStatus status = permissionService.hasPermission(parentNodeRef, PermissionService.CREATE_CHILDREN); if (status == AccessStatus.DENIED) { throw new AlfrescoRuntimeException(ERROR_PERMISSION_DENIED); } FileInfo mailFolder = serviceRegistry.getFileFolderService().create(parentNodeRef, folderName, ContentModel.TYPE_FOLDER); AlfrescoImapFolder resultFolder = new AlfrescoImapFolder( user.getQualifiedMailboxName(), mailFolder, folderName, getViewMode(mailboxName), root, getMountPointName(mailboxName), isExtractionEnabled(mailFolder.getNodeRef()), serviceRegistry); foldersCache.put(mailboxName, resultFolder); return resultFolder; } else { // folder already exists if (logger.isDebugEnabled()) { logger.debug("Folder '" + folderName + "' already exists"); } // next search from new parent parentNodeRef = child; } } throw new AlfrescoRuntimeException(ERROR_FOLDER_ALREADY_EXISTS); } 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 = getFolder(user, mailboxName); NodeRef nodeRef = folder.getFolderInfo().getNodeRef(); List 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 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."); } } foldersCache.remove(mailboxName); } public void renameMailbox(AlfrescoImapUser user, String oldMailboxName, String newMailboxName) { if (oldMailboxName == null || newMailboxName == null) { throw new IllegalArgumentException(ERROR_MAILBOX_NAME_IS_MANDATORY); } AlfrescoImapFolder sourceNode = getFolder(user, oldMailboxName); oldMailboxName = Utf7.decode(oldMailboxName, Utf7.UTF7_MODIFIED); newMailboxName = Utf7.decode(newMailboxName, Utf7.UTF7_MODIFIED); if (logger.isDebugEnabled()) { logger.debug("Renaming folder oldMailboxName=" + oldMailboxName + " newMailboxName=" + newMailboxName); } NodeRef root = getMailboxRootRef(oldMailboxName, user.getLogin()); String[] folderNames = getMailPathInRepo(newMailboxName).split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); String folderName = null; NodeRef parentNodeRef = root; // initial root for search try { for (int i = 0; i < folderNames.length; i++) { folderName = folderNames[i]; if (i == (folderNames.length - 1)) // is it the last element { FileInfo newFileInfo = null; 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. newFileInfo = fileFolderService.copy(sourceNode.getFolderInfo().getNodeRef(), parentNodeRef, folderName); } else { newFileInfo = fileFolderService.move( sourceNode.getFolderInfo().getNodeRef(), parentNodeRef, folderName); } foldersCache.remove(oldMailboxName); AlfrescoImapFolder resultFolder = new AlfrescoImapFolder( user.getQualifiedMailboxName(), newFileInfo, folderName, getViewMode(newMailboxName), root, getMountPointName(newMailboxName), isExtractionEnabled(newFileInfo.getNodeRef()), serviceRegistry); foldersCache.put(newMailboxName, resultFolder); } else { // not last element than checks if it exists and creates if doesn't NodeRef child = fileFolderService.searchSimple(parentNodeRef, folderName); if (child == null) { // check creation permission AccessStatus status = permissionService.hasPermission(parentNodeRef, PermissionService.CREATE_CHILDREN); if (status == AccessStatus.DENIED) { throw new AlfrescoRuntimeException(ERROR_PERMISSION_DENIED); } if (logger.isDebugEnabled()) { logger.debug("Creating folder '" + folderName + "'"); } serviceRegistry.getFileFolderService().create(parentNodeRef, folderName, ContentModel.TYPE_FOLDER); } else { parentNodeRef = child; if (logger.isDebugEnabled()) { logger.debug("Folder '" + folderName + "' already exists"); } } } } } catch (Exception e) { if (e instanceof AlfrescoRuntimeException) { throw (AlfrescoRuntimeException) e; } else { throw new AlfrescoRuntimeException(e.getMessage(), e); } } } public AlfrescoImapFolder getFolder(AlfrescoImapUser user, String mailboxName) { if (logger.isDebugEnabled()) { logger.debug("Folders cache size is " + foldersCache.getKeys().size()); } mailboxName = Utf7.decode(mailboxName, Utf7.UTF7_MODIFIED); if (logger.isDebugEnabled()) { logger.debug("Get folder '" + mailboxName + "'"); } // If MailFolder object is used to obtain hierarchy delimiter by LIST command: // Example: // C: 2 list "" "" // S: * LIST () "." "" // S: 2 OK LIST completed. if ("".equals(mailboxName)) { if (logger.isDebugEnabled()) { logger.debug("Request for the hierarchy delimiter"); } AlfrescoImapFolder hierarhyFolder = (AlfrescoImapFolder) foldersCache.get("hierarhy.delimeter"); if (logger.isDebugEnabled()) { logger.debug("Got a hierarhy delimeter from cache: " + hierarhyFolder); } if (hierarhyFolder == null) { hierarhyFolder = new AlfrescoImapFolder(user.getQualifiedMailboxName(), serviceRegistry); // TEMP Comment out putting this into the same cache as the "real folders" Causes NPE in // Security Interceptor //foldersCache.put("hierarhy.delimeter", hierarhyFolder); } return hierarhyFolder; } else if (AlfrescoImapConst.INBOX_NAME.equalsIgnoreCase(mailboxName) || AlfrescoImapConst.TRASH_NAME.equalsIgnoreCase(mailboxName)) { String cacheKey = user.getLogin() + '.' + mailboxName; AlfrescoImapFolder imapSystemFolder = (AlfrescoImapFolder) foldersCache.get(cacheKey); if(imapSystemFolder != null) { if (logger.isDebugEnabled()) { logger.debug("Got a system folder '" + mailboxName + "' from cache: " + imapSystemFolder); } /** * Check whether resultFolder is stale */ if(imapSystemFolder.isStale()) { logger.debug("system folder is stale"); imapSystemFolder = null; } } if (imapSystemFolder == null) { NodeRef userImapRoot = getUserImapHomeRef(user.getLogin()); NodeRef mailBoxRef = nodeService.getChildByName(userImapRoot, ContentModel.ASSOC_CONTAINS, mailboxName); if (mailBoxRef != null) { FileInfo mailBoxFileInfo = fileFolderService.getFileInfo(mailBoxRef); imapSystemFolder = new AlfrescoImapFolder( user.getQualifiedMailboxName(), mailBoxFileInfo, mailBoxFileInfo.getName(), getViewMode(mailboxName), userImapRoot, getMountPointName(mailboxName), isExtractionEnabled(mailBoxFileInfo.getNodeRef()), serviceRegistry); foldersCache.put(cacheKey, imapSystemFolder); if (logger.isDebugEnabled()) { logger.debug("Returning folder '" + mailboxName + "'"); } } else { if (logger.isDebugEnabled()) { logger.debug("Cannot get a folder '" + mailboxName + "'"); } throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] { mailboxName }); } } return imapSystemFolder; } /** * Folder is not hierarchy.delimiter or a "System" folder (INBOX or TRASH) */ AlfrescoImapFolder resultFolder = (AlfrescoImapFolder) foldersCache.get(mailboxName); if(resultFolder != null) { if (logger.isDebugEnabled()) { logger.debug("Got a folder '" + mailboxName + "' from cache: " + resultFolder); } /** * Check whether resultFolder is stale */ /* if(resultFolder.isStale()) { logger.debug("folder is stale"); resultFolder = null; } */ } if (resultFolder == null) { ImapViewMode viewMode = getViewMode(mailboxName); String mountPointName = getMountPointName(mailboxName); NodeRef root = getMailboxRootRef(mailboxName, user.getLogin()); NodeRef nodeRef = root; // initial top folder String[] folderNames = getMailPathInRepo(mailboxName).split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); if(folderNames.length == 1 && folderNames[0].length() == 0) { // This is the root of the mount point e.g "Alfresco IMAP" which has a path from root of "" FileInfo folderFileInfo = fileFolderService.getFileInfo(root); resultFolder = new AlfrescoImapFolder( user.getQualifiedMailboxName(), folderFileInfo, mountPointName, viewMode, root, mountPointName, isExtractionEnabled(folderFileInfo.getNodeRef()), serviceRegistry); if (logger.isDebugEnabled()) { logger.debug("Returning root folder '" + mailboxName + "'"); } } else { for (int i = 0; i < folderNames.length; i++) { if (logger.isDebugEnabled()) { logger.debug("Processing of '" + folderNames[i] + "'"); } NodeRef targetNode = fileFolderService.searchSimple(nodeRef, folderNames[i]); if (i == 0 && targetNode == null) { resultFolder = new AlfrescoImapFolder(user.getQualifiedMailboxName(), serviceRegistry); if (logger.isDebugEnabled()) { logger.debug("Returning empty folder '" + folderNames[i] + "'"); } // skip cache for root return resultFolder; } if (i == (folderNames.length - 1)) // is last { FileInfo folderFileInfo = fileFolderService.getFileInfo(targetNode); if (logger.isDebugEnabled()) { logger.debug("Found folder to list: " + folderFileInfo.getName()); } resultFolder = new AlfrescoImapFolder( user.getQualifiedMailboxName(), folderFileInfo, folderFileInfo.getName(), viewMode, root, mountPointName, isExtractionEnabled(folderFileInfo.getNodeRef()), serviceRegistry); if (logger.isDebugEnabled()) { logger.debug("Returning folder '" + mailboxName + "'"); } } /** * End of loop - next element in path */ nodeRef = targetNode; } } if (resultFolder == null) { if (logger.isDebugEnabled()) { logger.debug("Cannot get a folder '" + mailboxName + "'"); } throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] { mailboxName }); } else { foldersCache.put(mailboxName, resultFolder); if (logger.isDebugEnabled()) { logger.debug("Put a folder '" + mailboxName + "' to cache: " + resultFolder); } } } return resultFolder; } /** * Deep search for mailboxes/folders in the specified context * * Certain folders are excluded depending upon the view mode. * - For ARCHIVE mode all Share Sites are excluded. * - For MIXED and VIRTUAL non favourite sites are excluded. * * @param contextNodeRef context folder for search * @param viewMode is folder in "Virtual" View * @return list of mailboxes/folders */ private List searchDeep(final NodeRef contextNodeRef, final ImapViewMode viewMode) { if (logger.isDebugEnabled()) { logger.debug("[searchDeep] Start. nodeRef=" + contextNodeRef + ", viewMode=" + viewMode); } List searchResult = fileFolderService.listDeepFolders(contextNodeRef, new ImapSubFolderFilter(viewMode)); if (logger.isDebugEnabled()) { logger.debug("[searchDeep] End"); } return new ArrayList(searchResult); } /** * Shallow search for mailboxes in specified context * * @param contextNodeRef context folder for search * @param viewMode is folder in "Virtual" View * @param namePattern name pattern for search * @return list of mailboxes */ private List searchByPattern(final NodeRef contextNodeRef, final ImapViewMode viewMode, final String namePattern) { if (logger.isDebugEnabled()) { logger.debug("[searchByPattern] Start. nodeRef=" + contextNodeRef + ", viewMode=" + viewMode + " namePattern=" + namePattern); } List searchResult; /** * Shallow search for all folders below contextNodeRef */ if("*".equals(namePattern)) { /** * This is a simple listing of all folders below contextNodeRef */ logger.debug("call file folder service to list folders"); searchResult = fileFolderService.listFolders(contextNodeRef); } else { logger.debug("call listDeepFolders"); searchResult = fileFolderService.listDeepFolders(contextNodeRef, new ImapSubFolderFilter(viewMode, namePattern)); } // DO WE NEED TO WORRY ABOUT THE STUFF BELOW ? // WOULD ONLY BE RELEVANT if ContextNodeRef is site root. // IF contextNodeRef is the imap home or the contextNodeRef is in the depths of a non favourite site. // // Set result = new HashSet(searchResult); // if (viewMode == ImapViewMode.VIRTUAL || viewMode == ImapViewMode.MIXED) // { // /** // * In VIRTUAL and MIXED MODE WE SHOULD ONLY DISPLAY FAVOURITE SITES // */ // List nonFavSites = getNonFavouriteSites(getCurrentUser()); // for (SiteInfo siteInfo : nonFavSites) // { // FileInfo nonFavSite = fileFolderService.getFileInfo(siteInfo.getNodeRef()); // // // search deep for all folders in the site // List siteChilds = fileFolderService.search(nonFavSite.getNodeRef(), namePattern, false, true, true); // result.removeAll(siteChilds); // result.remove(nonFavSite); // } // // } // else // { // /** // * IN ARCHIVE MODE we don't display folders any SITES // */ // // Remove folders from Sites // List sites = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback>() // { // public List execute() throws Exception // { // List res = new ArrayList(); // try // { // // res = serviceRegistry.getSiteService().listSites(getCurrentUser()); // } // catch (SiteServiceException e) // { // // Do nothing. Root sites folder was not created. // if (logger.isWarnEnabled()) // { // logger.warn("Root sites folder was not created."); // } // } // catch (InvalidNodeRefException e) // { // // Do nothing. Root sites folder was deleted. // if (logger.isWarnEnabled()) // { // logger.warn("Root sites folder was deleted."); // } // } // // if (logger.isDebugEnabled()) // { // logger.debug("Search folders return "); // } // // return res; // } // }, false, true); // // for (SiteInfo siteInfo : sites) // { // List siteChilds = fileFolderService.search(siteInfo.getNodeRef(), namePattern, false, true, true); // result.removeAll(siteChilds); // // remove site // result.remove(fileFolderService.getFileInfo(siteInfo.getNodeRef())); // } // // } if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) { logger.debug("[searchByPattern] End. nodeRef=" + contextNodeRef + ", viewMode=" + viewMode + ", namePattern=" + namePattern + ", searchResult=" +searchResult.size()); } } return searchResult; } /** * 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 List searchMails(NodeRef contextNodeRef, ImapViewMode viewMode) { if (logger.isDebugEnabled()) { logger.debug("Search mails contextNodeRef=" + contextNodeRef + ", viewMode=" + viewMode ); } List searchResult = fileFolderService.listFiles(contextNodeRef); List result = new LinkedList(); //List searchResult = fileFolderService.search(contextNodeRef, namePattern, true, false, includeSubFolders); switch (viewMode) { case MIXED: result = searchResult; break; case ARCHIVE: for (FileInfo fileInfo : searchResult) { if (nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) { result.add(fileInfo); } } break; case VIRTUAL: for (FileInfo fileInfo : searchResult) { if (!nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) { result.add(fileInfo); } } break; } logger.debug("Found files:" + result.size()); return result; } public void subscribe(AlfrescoImapUser user, String mailbox) { if (logger.isDebugEnabled()) { logger.debug("Subscribing: " + user + ", " + mailbox); } AlfrescoImapFolder mailFolder = getFolder(user, mailbox); nodeService.removeAspect(mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSUBSCRIBED); } public void unsubscribe(AlfrescoImapUser user, String mailbox) { if (logger.isDebugEnabled()) { logger.debug("Unsubscribing: " + user + ", " + mailbox); } AlfrescoImapFolder mailFolder = getFolder(user, mailbox); if(mailFolder.getFolderInfo() != null) { logger.debug("Unsubscribing by ASPECT_IMAP_FOLDER_NONSUBSCRIBED"); nodeService.addAspect(mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSUBSCRIBED, null); } 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 synchronized Flags getFlags(FileInfo messageInfo) { Flags flags = new Flags(); if (nodeService.exists(messageInfo.getNodeRef())) { checkForFlaggableAspect(messageInfo.getNodeRef()); Map 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 synchronized 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 imapFolder. * * @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) { NodeRef nodeRef = messageInfo.getNodeRef(); checkForFlaggableAspect(nodeRef); AccessStatus status = permissionService.hasPermission(nodeRef, PermissionService.WRITE_PROPERTIES); if (status == AccessStatus.DENIED) { logger.debug("[setFlag] Access denied to add FLAG to " + nodeRef); //TODO should we throw an exception here? } else { nodeService.setProperty(messageInfo.getNodeRef(), flagToQname.get(flag), value); } } /** * Depend on listSubscribed param, list Mailboxes or list subscribed Mailboxes */ private List listMailboxes(AlfrescoImapUser user, String mailboxPattern, boolean listSubscribed) { if(logger.isDebugEnabled()) { logger.debug("[listMailboxes] user:" + user.getLogin() + ", mailboxPattern:" + mailboxPattern + ", listSubscribed:" + listSubscribed); } List result = new LinkedList(); Map mountPoints = getMountPoints(); NodeRef mountPoint; // List mailboxes that are in mount points for (String mountPointName : mountPoints.keySet()) { mountPoint = mountPoints.get(mountPointName); FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint); NodeRef mountParent = nodeService.getParentAssocs(mountPoint).get(0).getParentRef(); ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode(); /* FIX for ALF-2793 Reinstated if (!mailboxPattern.equals("*")) { mountPoint = mountParent; } */ List folders = expandFolder(mountPoint, mountPoint, user, mailboxPattern, listSubscribed, viewMode); if (folders != null) { for (AlfrescoImapFolder mailFolder : folders) { AlfrescoImapFolder folder = (AlfrescoImapFolder) mailFolder; folder.setMountPointName(mountPointName); folder.setViewMode(viewMode); folder.setMountParent(mountParent); } result.addAll(folders); } // Add mount point to the result list if (mailboxPattern.equals("*")) { if ((listSubscribed && isSubscribed(mountPointFileInfo, user.getLogin())) || (!listSubscribed)) { result.add( new AlfrescoImapFolder( user.getQualifiedMailboxName(), mountPointFileInfo, mountPointName, viewMode, mountParent, mountPointName, isExtractionEnabled(mountPointFileInfo.getNodeRef()), serviceRegistry)); } // \NoSelect else if (listSubscribed && hasSubscribedChild(mountPointFileInfo, user.getLogin(), viewMode)) { result.add( new AlfrescoImapFolder( user.getQualifiedMailboxName(), mountPointFileInfo, mountPointName, viewMode, mountParent, mountPointName, serviceRegistry, false, isExtractionEnabled(mountPointFileInfo.getNodeRef()))); } } } // List mailboxes that are in user IMAP Home NodeRef root = getUserImapHomeRef(user.getLogin()); List imapFolders = expandFolder(root, root, user, mailboxPattern, listSubscribed, ImapViewMode.ARCHIVE); if (imapFolders != null) { for (AlfrescoImapFolder mailFolder : imapFolders) { //AlfrescoImapFolder folder = (AlfrescoImapFolder) mailFolder; mailFolder.setViewMode(ImapViewMode.ARCHIVE); mailFolder.setMountParent(root); } result.addAll(imapFolders); } logger.debug("listMailboxes returning size:" + result.size()); StringBuilder prefix = new StringBuilder(128); prefix.append(ImapConstants.USER_NAMESPACE) .append(AlfrescoImapConst.HIERARCHY_DELIMITER) .append(user.getQualifiedMailboxName()) .append(AlfrescoImapConst.HIERARCHY_DELIMITER); int prefixLength = prefix.length(); for(AlfrescoImapFolder folder : result) { String cacheKey = folder.getFullName().substring(prefixLength + 1, folder.getFullName().length() - 1); logger.debug("[listMailboxes] Adding the cache entry : " + cacheKey); foldersCache.put(cacheKey, folder); } return result; } /** * Get the list of folders * * @param mailboxRoot * @param root * @param user * @param mailboxPattern * @param listSubscribed * @param viewMode * @return */ private List expandFolder( NodeRef mailboxRoot, NodeRef root, AlfrescoImapUser user, String mailboxPattern, boolean listSubscribed, ImapViewMode viewMode) { 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; String remainName = null; if (index < 0) { name = mailboxPattern; } else { name = mailboxPattern.substring(0, index); remainName = mailboxPattern.substring(index + 1); } if (logger.isDebugEnabled()) { logger.debug("Listing mailboxes: name=" + name); } if (index < 0) { // This is the last level if ("*".equals(name)) { // Deep listing of all folders Collection list = searchDeep(root, viewMode); if (listSubscribed) { list = getSubscribed(list, user.getLogin()); } if (list.size() > 0) { return createMailFolderList(user, list, mailboxRoot); } return null; } else if (name.endsWith("*")) { // Ends with wildcard List fullList = new LinkedList(); List list = searchByPattern(root, viewMode, name.replace('%', '*')); Collection subscribedList = list; if (listSubscribed) { subscribedList = getSubscribed(list, user.getLogin()); } if (list.size() > 0) { fullList.addAll(subscribedList); for (FileInfo fileInfo : list) { List childList = searchDeep(fileInfo.getNodeRef(), viewMode); if (listSubscribed) { fullList.addAll(getSubscribed(childList, user.getLogin())); } else { fullList.addAll(childList); } } return createMailFolderList(user, fullList, mailboxRoot); } return null; } else if ("%".equals(name)) { // Non recursive listing List subscribedList = new LinkedList(); List list = searchByPattern(root, viewMode, "*"); if (listSubscribed) { for (FileInfo fileInfo : list) { if (isSubscribed(fileInfo, user.getLogin())) { // folderName, viewMode, mountPointName will be set in listMailboxes() method subscribedList.add( new AlfrescoImapFolder( user.getQualifiedMailboxName(), fileInfo, null, null, mailboxRoot, null, isExtractionEnabled(fileInfo.getNodeRef()), serviceRegistry)); } // \NoSelect else if (hasSubscribedChild(fileInfo, user.getLogin(), viewMode)) { // folderName, viewMode, mountPointName will be set in listMailboxes() method subscribedList.add( new AlfrescoImapFolder( user.getQualifiedMailboxName(), fileInfo, null, null, mailboxRoot, null, serviceRegistry, false, isExtractionEnabled(fileInfo.getNodeRef()))); } } } else { return createMailFolderList(user, list, mailboxRoot); } return subscribedList; } else if (name.contains("%") || name.contains("*")) { // wild cards in the middle of the name List list = searchByPattern(root, viewMode, name.replace('%', '*')); Collection subscribedList = list; if (listSubscribed) { subscribedList = getSubscribed(list, user.getLogin()); } if (subscribedList.size() > 0) { return createMailFolderList(user, subscribedList, mailboxRoot); } return null; } else { // No wild cards List list = searchByPattern(root, viewMode, name); Collection subscribedList = list; if (listSubscribed) { subscribedList = getSubscribed(list, user.getLogin()); } if (subscribedList.size() > 0) { return createMailFolderList(user, subscribedList, mailboxRoot); } return null; } } // If (index != -1) this is not the last level List result = new LinkedList(); List list = searchByPattern(root, viewMode, name.replace('%', '*')); for (FileInfo folder : list) { Collection childFolders = expandFolder(mailboxRoot, folder.getNodeRef(), user, remainName, listSubscribed, viewMode); if (childFolders != null) { result.addAll(childFolders); } } return !result.isEmpty() ? result : null; } /** * Convert mailpath from IMAP client representation to the alfresco representation view * (e.g. with default settings * "getMailPathInRepo(Repository_virtual/Imap Home)" will return "Company Home/Imap Home") * * @param mailPath mailbox path in IMAP client * @return mailbox path in alfresco */ private String getMailPathInRepo(String mailPath) { if(logger.isDebugEnabled()) { logger.debug("[getMailPathInRepo] Path: " + mailPath); } String rootFolder; String remain = ""; int index = mailPath.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); if (index > 0) { // mail path contains a / rootFolder = mailPath.substring(0, index); remain = mailPath.substring(index + 1); } else { //mail path is a root folder rootFolder = mailPath; } if (imapConfigMountPoints.keySet().contains(rootFolder)) { //Map mountPoints = getMountPoints(); //NodeRef rootRef = mountPoints.get(rootFolder); String path = remain; if(logger.isDebugEnabled()) { logger.debug("[getMailPathInRepo] Mounted point returning: " + path); } return path; } else { if(logger.isDebugEnabled()) { logger.debug("[getMailPathInRepo] Not mounted. Returning path as is: " + mailPath); } return mailPath; } } /** * Return mount point name for the current mailbox. * * @param mailboxName mailbox name in IMAP client. * @return mount point name or null. */ private String getMountPointName(String mailboxName) { String rootFolder; int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); if (index > 0) { rootFolder = mailboxName.substring(0, index); } else { rootFolder = mailboxName; } if (imapConfigMountPoints.keySet().contains(rootFolder)) { return rootFolder; } else { return null; } } /** * Map of mount points. Name of mount point == key in the map. * * @return Map of mount points. */ private Map getMountPoints() { Set mountPointNodeRefs = new HashSet(5); Map mountPoints = new HashMap(); final NamespaceService namespaceService = serviceRegistry.getNamespaceService(); final SearchService searchService = serviceRegistry.getSearchService(); for (final ImapConfigMountPointsBean config : imapConfigMountPoints.values()) { try { // Get node reference. Do it in new transaction to avoid RollBack in case when AccessDeniedException is thrown. NodeRef nodeRef = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() { public NodeRef execute() throws Exception { try { return config.getFolderPath(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); if (nodeRef != null) { if (!mountPointNodeRefs.add(nodeRef)) { throw new IllegalArgumentException("A mount point has been defined twice: \n" + " Mount point: " + config); } mountPoints.put(config.getMountPointName(), nodeRef); } } 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 mountPoints; } /** * Get root reference for the specified mailbox * * @param mailboxName mailbox name in IMAP client. * @param userName * @return root reference for the specified mailbox */ public NodeRef getMailboxRootRef(String mailboxName, String userName) { String rootFolder; int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); if (index > 0) { rootFolder = mailboxName.substring(0, index); } else { rootFolder = mailboxName; } Map imapConfigs = imapConfigMountPoints; if (imapConfigs.keySet().contains(rootFolder)) { Map mountPoints = getMountPoints(); NodeRef mountRef = mountPoints.get(rootFolder); logger.debug("getMailboxRootRef mounted, " + mountRef); return mountRef; // MER EXPERIMENT //return nodeService.getParentAssocs(mountRef).get(0).getParentRef(); } else { NodeRef ret = getUserImapHomeRef(userName); logger.debug("getMailboxRootRef using userImapHome, " + ret); return ret; } } /** * 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. */ private NodeRef getUserImapHomeRef(final String userName) { NodeRef userHome = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { 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 boolean isSubscribed(FileInfo fileInfo, String userName) { return !nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSUBSCRIBED); } /** * getSubscribed filters out folders which are not subscribed. * @param list * @param userName * @return collection of subscribed folders. */ private Collection getSubscribed(Collection list, String userName) { Collection result = new LinkedList(); for (FileInfo folderInfo : list) { if (isSubscribed(folderInfo, userName)) { result.add(folderInfo); } } return result; } private boolean hasSubscribedChild(FileInfo parent, String userName, ImapViewMode viewMode) { List list = searchDeep(parent.getNodeRef(), viewMode); for (FileInfo fileInfo : list) { if (isSubscribed(fileInfo, userName)) { return true; } } return false; } private List createMailFolderList(AlfrescoImapUser user, Collection list, NodeRef imapUserHomeRef) { List result = new LinkedList(); // XXX : put folders into the cache and get them in next lookups. // The question is to get a mailBoxName for the cache key... And what if a folders list was changed? // So, for now keep it as is. for (FileInfo folderInfo : list) { // folderName, viewMode, mountPointName will be set in listSubscribedMailboxes() method result.add( new AlfrescoImapFolder( user.getQualifiedMailboxName(), folderInfo, null, null, imapUserHomeRef, null, isExtractionEnabled(folderInfo.getNodeRef()), serviceRegistry)); } return result; } /** * Return view mode ("virtual", "archive" or "mixed") for specified mailbox. * * @param mailboxName name of the mailbox in IMAP client. * @return view mode of the specified mailbox. */ private ImapViewMode getViewMode(String mailboxName) { String rootFolder; int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); if (index > 0) { rootFolder = mailboxName.substring(0, index); } else { rootFolder = mailboxName; } if (imapConfigMountPoints.keySet().contains(rootFolder)) { return imapConfigMountPoints.get(rootFolder).getMode(); } else { return ImapViewMode.ARCHIVE; } } 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 getFavouriteSites(final String userName) { if (logger.isDebugEnabled()) { logger.debug("[getFavouriteSites] entry for user: " + userName); } List 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(); PreferenceService preferenceService = (PreferenceService) serviceRegistry .getService(ServiceRegistry.PREFERENCE_SERVICE); Map prefs = preferenceService.getPreferences( userName, AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES); /** * List the user's sites */ List sites = serviceRegistry.getTransactionService() .getRetryingTransactionHelper().doInTransaction( new RetryingTransactionCallback>() { public List execute() throws Exception { List res = new ArrayList(); try { res = serviceRegistry.getSiteService() .listSites(userName); } catch (SiteServiceException e) { // Do nothing. Root sites folder was not // created. if (logger.isDebugEnabled()) { logger.warn("[getFavouriteSites] Root sites folder was not created."); } } catch (InvalidNodeRefException e) { // Do nothing. Root sites folder was // deleted. if (logger.isDebugEnabled()) { logger.warn("[getFavouriteSites] Root sites folder was deleted."); } } return res; } }, false, true); for (SiteInfo siteInfo : sites) { String key = AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES + "." + siteInfo.getShortName(); Boolean isImapFavourite = (Boolean) prefs.get(key); if (isImapFavourite != null && isImapFavourite) { 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 */ private void checkForFlaggableAspect(NodeRef nodeRef) { Set alreadyChecked = AlfrescoTransactionSupport.getResource(CHECKED_NODES); if (alreadyChecked == null) { alreadyChecked = new HashSet(); } 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(nodeRef, ContentModel.ASPECT_AUDITABLE); logger.debug("[checkForFlaggableAspect] Adding flaggable aspect to nodeRef: " + nodeRef); Map aspectProperties = new HashMap(); nodeService.addAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE, aspectProperties); } finally { policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); } } } 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(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); ResultSet resultSet = serviceRegistry.getSearchService().query(storeRef, "xpath", query); if (resultSet == null || resultSet.length() == 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 = resultSet.getNodeRef(0); NodeRef localisedSibling = serviceRegistry.getFileFolderService().getLocalizedSibling(defaultLocaleTemplate); result = localisedSibling.toString(); resultSet.close(); } // We are catching all exceptions. E.g. search service can possibly throw an exceptions on malformed queries. catch (Exception e) { logger.error("[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 typesToExclude; private List favs; private String mailboxPattern; private ImapViewMode imapViewMode; ImapSubFolderFilter(ImapViewMode imapViewMode) { this.imapViewMode = imapViewMode; this.typesToExclude = 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) { NodeRef folder = subfolderRef.getChildRef(); if (mailboxPattern != null) { logger.debug("Child QName: " + subfolderRef.getQName()); String name = (String) nodeService.getProperty(folder, ContentModel.PROP_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 :" + subfolderRef.getQName()); return true; } else { logger.debug("[ImapSubFolderFilter] (VIRTUAL) excluding non fav site folder :" + subfolderRef.getQName()); 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 :" + subfolderRef.getQName()); return false; } } return true; } } @Override public void onCreateChildAssociation(ChildAssociationRef childAssocRef, boolean isNewNode) { // Add a listener once, when a lots of messsages were created/moved into the folder if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) { AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(childAssocRef.getParentRef(), nodeService)); AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); } if (logger.isDebugEnabled()) { logger.debug("[onCreateChildAssociation] Association " + childAssocRef + " created. UIDVALIDITY will be changed."); } } public void onDeleteChildAssociation(ChildAssociationRef childAssocRef) { // Add a listener once, when a lots of messsages were created/moved into the folder if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) { AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(childAssocRef.getParentRef(), nodeService)); AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); } if (logger.isDebugEnabled()) { logger.debug("[onDeleteChildAssociation] Association " + childAssocRef + " removed. UIDVALIDITY will be changed."); } } public void beforeDeleteNode(NodeRef nodeRef) { NodeRef parentNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); if (ContentModel.TYPE_FOLDER.equals(nodeService.getType(nodeRef))) { // If a node is a folder, we need to remove its' cache with its' children cache as well invalidateFolderCacheByNodeRef(nodeRef, true); } else if (ContentModel.TYPE_CONTENT.equals(nodeService.getType(nodeRef))) { // If a node is a content, it is simpler to remove its' parent cache // to avoid of deal with folder messages cache invalidateFolderCacheByNodeRef(parentNodeRef, false); } else { return; } // Add a listener once, when a lots of messsages were created/moved into the folder if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) { AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(parentNodeRef, nodeService)); AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); } if (logger.isDebugEnabled()) { logger.debug("[beforeDeleteNode] Node " + nodeRef + " going to be removed. UIDVALIDITY will be changed for " + parentNodeRef); } } private class UidValidityTransactionListener extends TransactionListenerAdapter { private RunAsWork work; UidValidityTransactionListener(NodeRef folderNodeRef, NodeService nodeService) { this.work = new IncrementUidValidityWork(folderNodeRef, nodeService); } @Override public void afterCommit() { AuthenticationUtil.runAs(this.work, AuthenticationUtil.getSystemUserName()); } } private class IncrementUidValidityWork implements RunAsWork { private NodeService nodeService; private NodeRef folderNodeRef; public IncrementUidValidityWork(NodeRef folderNodeRef, NodeService nodeService) { this.folderNodeRef = folderNodeRef; this.nodeService = nodeService; } @Override public Long doWork() throws Exception { RetryingTransactionHelper txnHelper = serviceRegistry.getRetryingTransactionHelper(); return txnHelper.doInTransaction(new RetryingTransactionCallback(){ @Override public Long execute() throws Throwable { long modifDate = new Date().getTime(); if (!IncrementUidValidityWork.this.nodeService.hasAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER)) { Map aspectProperties = new HashMap(1, 1); aspectProperties.put(ImapModel.PROP_UIDVALIDITY, modifDate); IncrementUidValidityWork.this.nodeService.addAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER, aspectProperties); } else { IncrementUidValidityWork.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate); } if (logger.isDebugEnabled()) { logger.debug("UIDVALIDITY was modified for " + folderNodeRef); } return modifDate; } }, false, true); } } private void invalidateFolderCacheByNodeRef(NodeRef folderNodeRef, boolean invalidateChildren) { if (logger.isDebugEnabled()) { if (invalidateChildren) { logger.debug("[invalidateFolderCacheByNodeRef] Invalidate cache entries for " + folderNodeRef); } else { logger.debug("[invalidateFolderCacheByNodeRef] Invalidate cache entries for " + folderNodeRef + " and children"); } } if (invalidateChildren) { SimpleCache foldersCache = getFoldersCache(); List toRemove = new LinkedList(); for(Serializable name : foldersCache.getKeys()) { AlfrescoImapFolder folder = (AlfrescoImapFolder) foldersCache.get(name); if (folder == null || folderNodeRef.equals(folder.getFolderInfo().getNodeRef())) { toRemove.add(name); break; } } if (toRemove.size() > 0) { String rootName = (String) toRemove.get(0); for(Serializable name : foldersCache.getKeys()) { if (((String) name).startsWith(rootName)) { toRemove.add(name); } } if (logger.isDebugEnabled()) { logger.debug("Caches to invalidate: " + toRemove.toString()); } for(Serializable name : toRemove) { foldersCache.remove(name); } } } else { SimpleCache foldersCache = getFoldersCache(); for(Serializable name : foldersCache.getKeys()) { AlfrescoImapFolder folder = (AlfrescoImapFolder) foldersCache.get(name); if (folder == null || folderNodeRef.equals(folder.getFolderInfo().getNodeRef())) { foldersCache.remove(name); break; } } } } /** * Return true if provided nodeRef is in Sites/.../documentlibrary */ public boolean isNodeInSitesLibrary(NodeRef nodeRef) { boolean isInDocLibrary = false; NodeRef parent = nodeService.getPrimaryParent(nodeRef).getParentRef(); while (parent != null && !nodeService.getType(parent).equals(SiteModel.TYPE_SITE)) { String parentName = (String) nodeService.getProperty(parent, ContentModel.PROP_NAME); if (parentName.equalsIgnoreCase("documentlibrary")) { isInDocLibrary = true; } nodeRef = parent; if (nodeService.getPrimaryParent(nodeRef) != null) { parent = nodeService.getPrimaryParent(nodeRef).getParentRef(); } } if (parent == null) { return false; } else { return nodeService.getType(parent).equals(SiteModel.TYPE_SITE) && isInDocLibrary; } } /** * Extract attachments from a MimeMessage * * Puts the attachments into a subfolder below the parent folder. * * @return the node ref of the folder containing the attachments or null if there are no * attachments. */ public NodeRef extractAttachments( NodeRef parentFolder, NodeRef messageFile, MimeMessage originalMessage) throws IOException, MessagingException { String messageName = (String)nodeService.getProperty(messageFile, ContentModel.PROP_NAME); String attachmentsFolderName = messageName + "-attachments"; FileInfo attachmentsFolderFileInfo = null; Object content = originalMessage.getContent(); if (content instanceof Multipart) { Multipart multipart = (Multipart) content; for (int i = 0, n = multipart.getCount(); i < n; i++) { Part part = multipart.getBodyPart(i); if ("attachment".equalsIgnoreCase(part.getDisposition())) { if (attachmentsFolderFileInfo == null) { attachmentsFolderFileInfo = fileFolderService.create( parentFolder, attachmentsFolderName, ContentModel.TYPE_FOLDER); nodeService.createAssociation( messageFile, attachmentsFolderFileInfo.getNodeRef(), ImapModel.ASSOC_IMAP_ATTACHMENTS_FOLDER); } createAttachment(messageFile, attachmentsFolderFileInfo.getNodeRef(), part); } } } if(attachmentsFolderFileInfo != null) { return attachmentsFolderFileInfo.getNodeRef(); } else { return null; } } /** * Create an attachment given a mime part * * @param messageFile the file containing the message * @param destinationFolder where to put the attachment * @param part the mime part * * @throws MessagingException * @throws IOException */ private void createAttachment(NodeRef messageFile, NodeRef destinationFolder, Part part) throws MessagingException, IOException { String fileName = part.getFileName(); try { fileName = MimeUtility.decodeText(fileName); } catch (UnsupportedEncodingException e) { if (logger.isWarnEnabled()) { logger.warn("Cannot decode file name '" + fileName + "'", e); } } ContentType contentType = new ContentType(part.getContentType()); if(contentType.getBaseType().equalsIgnoreCase("application/ms-tnef")) { // The content is TNEF HMEFMessage hmef = new HMEFMessage(part.getInputStream()); //hmef.getBody(); List attachments = hmef.getAttachments(); for(org.apache.poi.hmef.Attachment attachment : attachments) { String subName = attachment.getLongFilename(); NodeRef attachmentNode = fileFolderService.searchSimple(destinationFolder, subName); if (attachmentNode == null) { /* * If the node with the given name does not already exist * Create the content node to contain the attachment */ FileInfo createdFile = fileFolderService.create( destinationFolder, subName, ContentModel.TYPE_CONTENT); attachmentNode = createdFile.getNodeRef(); serviceRegistry.getNodeService().createAssociation( messageFile, attachmentNode, ImapModel.ASSOC_IMAP_ATTACHMENT); byte[] bytes = attachment.getContents(); ContentWriter writer = fileFolderService.getWriter(attachmentNode); //TODO ENCODING - attachment.getAttribute(TNEFProperty.); String extension = attachment.getExtension(); String mimetype = mimetypeService.getMimetype(extension); if(mimetype != null) { writer.setMimetype(mimetype); } OutputStream os = writer.getContentOutputStream(); ByteArrayInputStream is = new ByteArrayInputStream(bytes); FileCopyUtils.copy(is, os); } } } else { // not TNEF NodeRef attachmentNode = fileFolderService.searchSimple(destinationFolder, fileName); if (attachmentNode == null) { /* * If the node with the given name does not already exist * Create the content node to contain the attachment */ FileInfo createdFile = fileFolderService.create( destinationFolder, fileName, ContentModel.TYPE_CONTENT); attachmentNode = createdFile.getNodeRef(); serviceRegistry.getNodeService().createAssociation( messageFile, attachmentNode, ImapModel.ASSOC_IMAP_ATTACHMENT); // the part is a normal IMAP attachment ContentWriter writer = fileFolderService.getWriter(attachmentNode); writer.setMimetype(contentType.getBaseType()); String charset = contentType.getParameter("charset"); if(charset != null) { writer.setEncoding(charset); } OutputStream os = writer.getContentOutputStream(); FileCopyUtils.copy(part.getInputStream(), os); } } } }