diff --git a/config/alfresco/bootstrap/sitesSpace.xml b/config/alfresco/bootstrap/sitesSpace.xml index 52f078b394..a4647aa342 100644 --- a/config/alfresco/bootstrap/sitesSpace.xml +++ b/config/alfresco/bootstrap/sitesSpace.xml @@ -1,6 +1,6 @@ - + @@ -19,6 +19,9 @@ surf-config + + + diff --git a/config/alfresco/cmis-api-context.xml b/config/alfresco/cmis-api-context.xml index f996aabf7d..07da53f728 100644 --- a/config/alfresco/cmis-api-context.xml +++ b/config/alfresco/cmis-api-context.xml @@ -31,6 +31,7 @@ + diff --git a/config/alfresco/import-export-context.xml b/config/alfresco/import-export-context.xml index 315b16fad7..135d3b3162 100644 --- a/config/alfresco/import-export-context.xml +++ b/config/alfresco/import-export-context.xml @@ -61,6 +61,9 @@ + + + diff --git a/config/alfresco/model-specific-services-context.xml b/config/alfresco/model-specific-services-context.xml index 92a662a16f..3eaf48cdd4 100644 --- a/config/alfresco/model-specific-services-context.xml +++ b/config/alfresco/model-specific-services-context.xml @@ -10,6 +10,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -18,7 +48,8 @@ - + + @@ -90,24 +121,13 @@ - - - - - - - - - - - - + - - + + @@ -116,7 +136,7 @@ - + diff --git a/config/alfresco/model/systemModel.xml b/config/alfresco/model/systemModel.xml index c8433cc05a..d67a5b859c 100644 --- a/config/alfresco/model/systemModel.xml +++ b/config/alfresco/model/systemModel.xml @@ -161,6 +161,14 @@ Hidden false + + + + d:int + false + 0 + + diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index 73a0ae96ab..8bc6794f4d 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -2758,6 +2758,7 @@ + sitestore /alfresco/site-data diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 4ef3de993d..b6b231c612 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -432,7 +432,6 @@ org.alfresco.service.cmr.model.FileFolderService.getWriter=ACL_NODE.0.sys:base.WriteContent org.alfresco.service.cmr.model.FileFolderService.exists=ACL_ALLOW org.alfresco.service.cmr.model.FileFolderService.getType=ACL_ALLOW - org.alfresco.service.cmr.model.FileFolderService.removeHiddenFiles=ACL_ALLOW org.alfresco.service.cmr.model.FileFolderService.*=ACL_DENY diff --git a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml index 386def57c7..1b320769aa 100644 --- a/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml +++ b/config/alfresco/subsystems/fileServers/default/network-protocol-context.xml @@ -290,6 +290,7 @@ + {http://www.alfresco.org/model/forum/1.0}forum diff --git a/source/java/org/alfresco/cmis/mapping/CMISServicesImpl.java b/source/java/org/alfresco/cmis/mapping/CMISServicesImpl.java index d9fd68ea75..270d04eae3 100644 --- a/source/java/org/alfresco/cmis/mapping/CMISServicesImpl.java +++ b/source/java/org/alfresco/cmis/mapping/CMISServicesImpl.java @@ -67,6 +67,7 @@ import org.alfresco.query.EmptyPagingResults; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; import org.alfresco.repo.model.Repository; +import org.alfresco.repo.model.filefolder.HiddenAspect; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; import org.alfresco.repo.search.QueryParameterDefImpl; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -107,6 +108,8 @@ import org.alfresco.service.cmr.version.VersionType; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -351,6 +354,11 @@ public class CMISServicesImpl implements CMISServices, ApplicationContextAware, { lifecycle.setApplicationContext(applicationContext); } + + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.hiddenAspect = hiddenAspect; + } /* (non-Javadoc) * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) @@ -587,6 +595,8 @@ public class CMISServicesImpl implements CMISServices, ApplicationContextAware, } } + private HiddenAspect hiddenAspect; + /* * (non-Javadoc) * @see org.alfresco.cmis.CMISServices#getChildren(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.cmis.CMISTypesFilterEnum, java.lang.String) @@ -595,12 +605,14 @@ public class CMISServicesImpl implements CMISServices, ApplicationContextAware, throws CMISInvalidArgumentException { PagingResults pageOfNodeInfos = getChildren(folderNodeRef, typesFilter, BigInteger.valueOf(Integer.MAX_VALUE), BigInteger.valueOf(0), orderBy); - - int pageCnt = pageOfNodeInfos.getPage().size(); + +// List filteredChildren = hiddenAspect.removeHiddenFiles(Client.cmis, pageOfNodeInfos.getPage()); + List filteredChildren = pageOfNodeInfos.getPage(); + int pageCnt = filteredChildren.size(); NodeRef[] result = new NodeRef[pageCnt]; - + int idx = 0; - for (FileInfo child : pageOfNodeInfos.getPage()) + for (FileInfo child : filteredChildren) { result[idx] = child.getNodeRef(); idx++; @@ -694,14 +706,22 @@ public class CMISServicesImpl implements CMISServices, ApplicationContextAware, // numItems may be // returned - PagingResults result = fileFolderService.list(folderNodeRef, listFiles, listFolders, null, sortProps, pageRequest); - - if (logger.isDebugEnabled()) + FileFilterMode.setClient(Client.cmis); + try { - logger.debug("getChildren: " + result.getPage().size() + " in " + (System.currentTimeMillis() - start) + " msecs"); + PagingResults result = fileFolderService.list(folderNodeRef, listFiles, listFolders, null, sortProps, pageRequest); + + if (logger.isDebugEnabled()) + { + logger.debug("getChildren: " + result.getPage().size() + " in " + (System.currentTimeMillis() - start) + " msecs"); + } + + return result; + } + finally + { + FileFilterMode.clearClient(); } - - return result; } /* diff --git a/source/java/org/alfresco/filesys/repo/CifsHelper.java b/source/java/org/alfresco/filesys/repo/CifsHelper.java index 7ee260c1e5..5a3469927f 100644 --- a/source/java/org/alfresco/filesys/repo/CifsHelper.java +++ b/source/java/org/alfresco/filesys/repo/CifsHelper.java @@ -36,6 +36,8 @@ import org.alfresco.jlan.server.filesys.FileName; import org.alfresco.jlan.server.filesys.FileType; import org.alfresco.jlan.util.WildCard; import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.filefolder.HiddenAspect; +import org.alfresco.repo.model.filefolder.HiddenAspect.Visibility; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileFolderUtil; @@ -49,6 +51,7 @@ import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; +import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.SearchLanguageConversion; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -70,6 +73,7 @@ public class CifsHelper private FileFolderService fileFolderService; private MimetypeService mimetypeService; private PermissionService permissionService; + private HiddenAspect hiddenAspect; private Set excludedTypes = new HashSet(); @@ -107,6 +111,11 @@ public class CifsHelper this.permissionService = permissionService; } + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.hiddenAspect = hiddenAspect; + } + /** * Return the node service * @@ -285,10 +294,9 @@ public class CifsHelper if (name != null) { fileInfo.setFileName(name); - + // Check for file names that should be hidden - - if(nodeService.hasAspect(fileInfo.getNodeRef(), ContentModel.ASPECT_HIDDEN)) + if(hiddenAspect.getVisibility(Client.cifs, fileInfo.getNodeRef()) == Visibility.HiddenAttribute) { // Add the hidden file attribute int attr = fileInfo.getFileAttributes(); diff --git a/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java b/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java index 5f12189fb9..dbfa6db3d7 100644 --- a/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java +++ b/source/java/org/alfresco/filesys/repo/CommandExecutorImpl.java @@ -6,10 +6,7 @@ import java.util.List; import org.alfresco.filesys.alfresco.ExtendedDiskInterface; import org.alfresco.filesys.alfresco.RepositoryDiskInterface; -import org.alfresco.filesys.repo.FilesystemTransactionAdvice.PropagatingException; import org.alfresco.filesys.repo.rules.Command; -import org.alfresco.filesys.repo.rules.Operation; -import org.alfresco.filesys.repo.rules.OperationExecutor; import org.alfresco.filesys.repo.rules.commands.CloseFileCommand; import org.alfresco.filesys.repo.rules.commands.CompoundCommand; import org.alfresco.filesys.repo.rules.commands.CopyContentCommand; @@ -22,23 +19,17 @@ import org.alfresco.filesys.repo.rules.commands.RemoveNoContentFileOnError; import org.alfresco.filesys.repo.rules.commands.RemoveTempFileCommand; import org.alfresco.filesys.repo.rules.commands.RenameFileCommand; import org.alfresco.filesys.repo.rules.commands.ReturnValueCommand; -import org.alfresco.filesys.repo.rules.operations.CreateFileOperation; -import org.alfresco.filesys.repo.rules.operations.DeleteFileOperation; -import org.alfresco.filesys.repo.rules.operations.RenameFileOperation; import org.alfresco.jlan.server.SrvSession; -import org.alfresco.jlan.server.filesys.AccessMode; -import org.alfresco.jlan.server.filesys.FileAction; -import org.alfresco.jlan.server.filesys.FileAttribute; -import org.alfresco.jlan.server.filesys.FileOpenParams; import org.alfresco.jlan.server.filesys.TreeConnection; -import org.alfresco.jlan.smb.SharingMode; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; /** * Content Disk Driver Command Executor @@ -164,6 +155,27 @@ public class CommandExecutorImpl implements CommandExecutor return ret; } + private Client getClient(SrvSession srvSession) + { + String clientStr = srvSession.getServer().getProtocolName().toLowerCase(); + if(clientStr.equals("cifs")) + { + return Client.cifs; + } + else if(clientStr.equals("nfs")) + { + return Client.nfs; + } + else if(clientStr.equals("ftp")) + { + return Client.ftp; + } + else + { + throw new IllegalArgumentException(); + } + } + /** * @param sess * @param tree @@ -174,122 +186,129 @@ public class CommandExecutorImpl implements CommandExecutor */ private Object executeInternal(SrvSession sess, TreeConnection tree, Command command, Object result) throws IOException { - if(command instanceof CompoundCommand) + FileFilterMode.setClient(getClient(sess)); + try { - Object ret = null; - logger.debug("compound command received"); - CompoundCommand x = (CompoundCommand)command; - - for(Command compoundPart : x.getCommands()) - { - logger.debug("running part of compound command"); - Object val = executeInternal(sess, tree, compoundPart, result); - if(val != null) + if(command instanceof CompoundCommand) + { + Object ret = null; + logger.debug("compound command received"); + CompoundCommand x = (CompoundCommand)command; + + for(Command compoundPart : x.getCommands()) { - // Return the value from the last command. - ret = val; + logger.debug("running part of compound command"); + Object val = executeInternal(sess, tree, compoundPart, result); + if(val != null) + { + // Return the value from the last command. + ret = val; + } } - } - return ret; - } - else if(command instanceof CreateFileCommand) - { - logger.debug("create file command"); - CreateFileCommand create = (CreateFileCommand)command; - return repositoryDiskInterface.createFile(create.getRootNode(), create.getPath(), create.getAllocationSize()); - } - else if(command instanceof DeleteFileCommand) - { - logger.debug("delete file command"); - DeleteFileCommand delete = (DeleteFileCommand)command; - diskInterface.deleteFile(sess, tree, delete.getPath()); - } - else if(command instanceof OpenFileCommand) - { - logger.debug("open file command"); - OpenFileCommand o = (OpenFileCommand)command; - - OpenFileMode mode = o.getMode(); - return repositoryDiskInterface.openFile(sess, tree, o.getRootNodeRef(), o.getPath(), mode, o.isTruncate()); - - } - else if(command instanceof CloseFileCommand) - { - logger.debug("close file command"); - CloseFileCommand c = (CloseFileCommand)command; - repositoryDiskInterface.closeFile(c.getRootNodeRef(), c.getPath(), c.getNetworkFile()); - } - else if(command instanceof ReduceQuotaCommand) - { - logger.debug("reduceQuota file command"); - ReduceQuotaCommand r = (ReduceQuotaCommand)command; - repositoryDiskInterface.reduceQuota(sess, tree, r.getNetworkFile()); - } - else if(command instanceof RenameFileCommand) - { - logger.debug("rename command"); - RenameFileCommand rename = (RenameFileCommand)command; - diskInterface.renameFile(sess, tree, rename.getFromPath(), rename.getToPath()); - } - else if(command instanceof CopyContentCommand) - { - if(logger.isDebugEnabled()) - { - logger.debug("Copy content command - copy content"); + return ret; } - CopyContentCommand copy = (CopyContentCommand)command; - repositoryDiskInterface.copyContent(copy.getRootNode(), copy.getFromPath(), copy.getToPath()); - } - else if(command instanceof DoNothingCommand) - { - if(logger.isDebugEnabled()) + else if(command instanceof CreateFileCommand) { - logger.debug("Do Nothing Command - doing nothing"); + logger.debug("create file command"); + CreateFileCommand create = (CreateFileCommand)command; + return repositoryDiskInterface.createFile(create.getRootNode(), create.getPath(), create.getAllocationSize()); + } + else if(command instanceof DeleteFileCommand) + { + logger.debug("delete file command"); + DeleteFileCommand delete = (DeleteFileCommand)command; + diskInterface.deleteFile(sess, tree, delete.getPath()); + } + else if(command instanceof OpenFileCommand) + { + logger.debug("open file command"); + OpenFileCommand o = (OpenFileCommand)command; + + OpenFileMode mode = o.getMode(); + return repositoryDiskInterface.openFile(sess, tree, o.getRootNodeRef(), o.getPath(), mode, o.isTruncate()); + + } + else if(command instanceof CloseFileCommand) + { + logger.debug("close file command"); + CloseFileCommand c = (CloseFileCommand)command; + repositoryDiskInterface.closeFile(c.getRootNodeRef(), c.getPath(), c.getNetworkFile()); + } + else if(command instanceof ReduceQuotaCommand) + { + logger.debug("reduceQuota file command"); + ReduceQuotaCommand r = (ReduceQuotaCommand)command; + repositoryDiskInterface.reduceQuota(sess, tree, r.getNetworkFile()); + } + else if(command instanceof RenameFileCommand) + { + logger.debug("rename command"); + RenameFileCommand rename = (RenameFileCommand)command; + diskInterface.renameFile(sess, tree, rename.getFromPath(), rename.getToPath()); + } + else if(command instanceof CopyContentCommand) + { + if(logger.isDebugEnabled()) + { + logger.debug("Copy content command - copy content"); + } + CopyContentCommand copy = (CopyContentCommand)command; + repositoryDiskInterface.copyContent(copy.getRootNode(), copy.getFromPath(), copy.getToPath()); + } + else if(command instanceof DoNothingCommand) + { + if(logger.isDebugEnabled()) + { + logger.debug("Do Nothing Command - doing nothing"); + } + } + else if(command instanceof ResultCallback) + { + if(logger.isDebugEnabled()) + { + logger.debug("Result Callback"); + } + ResultCallback callback = (ResultCallback)command; + callback.execute(result); + } + else if(command instanceof RemoveTempFileCommand) + { + RemoveTempFileCommand r = (RemoveTempFileCommand)command; + if(logger.isDebugEnabled()) + { + logger.debug("Remove Temp File:" + r.getNetworkFile()); + } + File file = r.getNetworkFile().getFile(); + boolean isDeleted = file.delete(); + + if(!isDeleted) + { + logger.debug("unable to delete temp file:" + r.getNetworkFile() + ", closed="+ r.getNetworkFile().isClosed()); + } + } + else if(command instanceof ReturnValueCommand) + { + ReturnValueCommand r = (ReturnValueCommand)command; + if(logger.isDebugEnabled()) + { + logger.debug("Return value"); + } + return r.getReturnValue(); + } + else if(command instanceof RemoveNoContentFileOnError) + { + RemoveNoContentFileOnError r = (RemoveNoContentFileOnError)command; + if(logger.isDebugEnabled()) + { + logger.debug("Remove no content file on error"); + } + repositoryDiskInterface.deleteEmptyFile(r.getRootNodeRef(), r.getPath()); } } - else if(command instanceof ResultCallback) + finally { - if(logger.isDebugEnabled()) - { - logger.debug("Result Callback"); - } - ResultCallback callback = (ResultCallback)command; - callback.execute(result); + FileFilterMode.clearClient(); } - else if(command instanceof RemoveTempFileCommand) - { - RemoveTempFileCommand r = (RemoveTempFileCommand)command; - if(logger.isDebugEnabled()) - { - logger.debug("Remove Temp File:" + r.getNetworkFile()); - } - File file = r.getNetworkFile().getFile(); - boolean isDeleted = file.delete(); - - if(!isDeleted) - { - logger.debug("unable to delete temp file:" + r.getNetworkFile() + ", closed="+ r.getNetworkFile().isClosed()); - } - } - else if(command instanceof ReturnValueCommand) - { - ReturnValueCommand r = (ReturnValueCommand)command; - if(logger.isDebugEnabled()) - { - logger.debug("Return value"); - } - return r.getReturnValue(); - } - else if(command instanceof RemoveNoContentFileOnError) - { - RemoveNoContentFileOnError r = (RemoveNoContentFileOnError)command; - if(logger.isDebugEnabled()) - { - logger.debug("Remove no content file on error"); - } - repositoryDiskInterface.deleteEmptyFile(r.getRootNodeRef(), r.getPath()); - } - return null; } diff --git a/source/java/org/alfresco/repo/admin/patch/impl/AVMToADMRemoteStorePatch.java b/source/java/org/alfresco/repo/admin/patch/impl/AVMToADMRemoteStorePatch.java index 50387a8135..8811f8bb65 100644 --- a/source/java/org/alfresco/repo/admin/patch/impl/AVMToADMRemoteStorePatch.java +++ b/source/java/org/alfresco/repo/admin/patch/impl/AVMToADMRemoteStorePatch.java @@ -40,6 +40,7 @@ import org.alfresco.repo.admin.patch.AbstractPatch; import org.alfresco.repo.batch.BatchProcessWorkProvider; import org.alfresco.repo.batch.BatchProcessor; import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker; +import org.alfresco.repo.model.filefolder.HiddenAspect; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.avm.AVMNodeDescriptor; @@ -112,6 +113,7 @@ public class AVMToADMRemoteStorePatch extends AbstractPatch private SiteService siteService; private AVMService avmService; private RuleService ruleService; + private HiddenAspect hiddenAspect; private String avmStore; private String avmRootPath = "/"; @@ -175,6 +177,11 @@ public class AVMToADMRemoteStorePatch extends AbstractPatch } } + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.hiddenAspect = hiddenAspect; + } + @Override protected void checkProperties() { @@ -624,9 +631,9 @@ public class AVMToADMRemoteStorePatch extends AbstractPatch ChildAssociationRef ref = this.nodeService.createNode( rootRef, ContentModel.ASSOC_CONTAINS, assocQName, ContentModel.TYPE_FOLDER, properties); surfConfigRef = ref.getChildRef(); - Map aspectProperties = new HashMap(1, 1.0f); - aspectProperties.put(ContentModel.PROP_IS_INDEXED, false); - this.nodeService.addAspect(surfConfigRef, ContentModel.ASPECT_INDEX_CONTROL, aspectProperties); + + // surf-config needs to be hidden + hiddenAspect.hideNode(ref.getChildRef()); } catch (DuplicateChildNodeNameException dupErr) { diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index 35ce5cac40..e2631a9330 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -1,2053 +1,2074 @@ -/* - * Copyright (C) 2005-2011 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 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.Arrays; -import java.util.Collection; -import java.util.Collections; -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.NavigableMap; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import javax.mail.Flags; -import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.Part; -import javax.mail.Flags.Flag; -import javax.mail.internet.AddressException; -import javax.mail.internet.ContentType; -import javax.mail.internet.InternetAddress; -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.node.NodeServicePolicies.OnUpdatePropertiesPolicy; -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.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.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.ChildAssociationRef; -import org.alfresco.service.cmr.repository.ContentData; -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.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.EqualsHelper; -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.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.store.SimpleStoredMessage; - -/** - * @author Dmitry Vaserin - * @author Arseny Kovalchuk - * @author David Ward - * @since 3.2 - */ -public class ImapServiceImpl implements ImapService, 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 MimetypeService mimetypeService; - private NamespaceService namespaceService; - private SearchService searchService; - - // 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, FolderStatus> folderCache; - private int folderCacheSize = 1000; - private ReentrantReadWriteLock folderCacheLock = new ReentrantReadWriteLock(); - private SimpleCache messageCache; - private Map imapConfigMountPoints; - private Map mountPointIds; - 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) - { - service.startupInTxn(false); - } - - @Override - protected void onShutdown(ApplicationEvent event) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @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 messageCache) - { - this.messageCache = messageCache; - } - - 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 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( - imapConfigMountPointsBeans.length * 2); - this.mountPointIds = new HashMap(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 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); - PropertyCheck.mandatory(this, "namespaceService", namespaceService); - PropertyCheck.mandatory(this, "searchService", getSearchService()); - this.folderCache = new MaxSizeMap, 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(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() - { - @Override - public Void doWork() throws Exception - { - List mailboxes = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction( - new RetryingTransactionCallback>() - { - @Override - public List execute() throws Throwable - { - startup(); - - List result = new LinkedList(); - - // 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)); - } - - // ---------------------- 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)) - { - fileFolderService.delete(fileInfo.getNodeRef()); - messageCache.remove(fileInfo.getNodeRef()); - } - } - - 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(), serviceRegistry); - } - final NodeRef root; - final List pathElements; - ImapViewMode viewMode = ImapViewMode.ARCHIVE; - int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); - int mountPointId = 0; - if (index < 0) - { - root = getUserImapHomeRef(user.getLogin()); - pathElements = Collections.singletonList(mailboxName); - } - else - { - String rootPath = mailboxName.substring(0, index); - ImapConfigMountPointsBean imapConfigMountPoint = this.imapConfigMountPoints.get(rootPath); - if (imapConfigMountPoint != null) - { - mountPointId = this.mountPointIds.get(rootPath); - root = imapConfigMountPoint.getFolderPath(serviceRegistry.getNamespaceService(), nodeService, searchService, fileFolderService); - 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 - { - mailFolder = fileFolderService.resolveNamePath(root, pathElements, !mayCreate); - } - catch (FileNotFoundException e) - { - throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] - { - mailboxName - }); - } - if (mailFolder == null) - { - if (!mayCreate) - { - throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] - { - mailboxName - }); - } - if (logger.isDebugEnabled()) - { - logger.debug("Creating mailbox: " + mailboxName); - } - mailFolder = FileFolderUtil.makeFolders(fileFolderService, root, pathElements, ContentModel.TYPE_FOLDER); - } - else - { - if (!mayExist) - { - throw new AlfrescoRuntimeException(ERROR_FOLDER_ALREADY_EXISTS); - } - } - return new AlfrescoImapFolder(mailFolder, user.getLogin(), pathElements.get(pathElements.size() - 1), mailboxName, viewMode, - 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 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."); - } - } - } - - 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() - { - @Override - public String doWork() throws Exception - { - return (String) nodeService.getProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN); - } - }, AuthenticationUtil.getSystemUserName()); - - Pair cacheKey = null; - if (changeToken != null) - { - cacheKey = new Pair(userName, changeToken); - this.folderCacheLock.readLock().lock(); - try - { - FolderStatus result = this.folderCache.get(cacheKey); - if (result != null) - { - return result; - } - } - finally - { - this.folderCacheLock.readLock().unlock(); - } - } - List fileInfos = fileFolderService.removeHiddenFiles(fileFolderService.listFiles(contextNodeRef)); - final NavigableMap currentSearch = new TreeMap(); - - 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(userName, changeToken); - final String finalToken = changeToken; - doAsSystem(new RunAsWork() - { - @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); - 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 = getOrCreateMailbox(user, mailbox, true, false); - 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 Flags getFlags(FileInfo messageInfo) - { - Flags flags = new Flags(); - 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 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) - { - setFlag(messageInfo.getNodeRef(), flag, value); - } - - private void setFlag(NodeRef nodeRef, Flag flag, boolean value) - { - 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 - { - 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); - } - - /** - * Depend on listSubscribed param, list Mailboxes or list subscribed Mailboxes - */ - public 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(); - - // 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; - - 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) - { - String userName = user.getLogin(); - if (!listSubscribed || isSubscribed(mountPointFileInfo, userName)) - { - result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, - isExtractionEnabled(mountPointFileInfo.getNodeRef()), serviceRegistry, mountPointId)); - } - else if (rootPath.endsWith("%") && !expandFolder(mountPoint, user, mountPointName, "%", true, viewMode, mountPointId).isEmpty()) // \NoSelect - { - result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, - 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 - */ - private List 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 fullList = new LinkedList(); - ImapSubFolderFilter filter = new ImapSubFolderFilter(viewMode, name.replace('%', '*')); - List list; - // Only list this folder if we have a wildcard name. Otherwise do a direct lookup by name. - if (name.contains("*") || name.contains("%")) - { - list = fileFolderService.removeHiddenFiles(fileFolderService.listFolders(root)); - } - else - { - NodeRef nodeRef = fileFolderService.searchSimple(root, name); - FileInfo fileInfo; - list = nodeRef == null || !(fileInfo = fileFolderService.getFileInfo(nodeRef)).isFolder() ? Collections.emptyList() : Collections.singletonList(fileInfo); - } - - if (index < 0) - { - // This is the last level - for (FileInfo fileInfo : list) - { - if (!filter.isEnterSubfolder(fileInfo.getNodeRef())) - { - continue; - } - String folderPath = rootPathPrefix + fileInfo.getName(); - String userName = user.getLogin(); - if (!listSubscribed || isSubscribed(fileInfo, userName)) - { - fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode, - isExtractionEnabled(fileInfo.getNodeRef()), 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, - 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() - { - 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); - } - 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() - { - 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); - } - - 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); - - NodeRef rootNode = nodeService.getRootNode(storeRef); - - List 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 typesToExclude; - private List 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, nodeService); - AlfrescoTransactionSupport.bindListener(txnListener); - AlfrescoTransactionSupport.bindResource(key, txnListener); - } - return txnListener; - } - - @Override - public void onCreateChildAssociation(final ChildAssociationRef childAssocRef, boolean isNewNode) - { - doAsSystem(new RunAsWork() - { - @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() - { - @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 before, - final Map after) - { - doAsSystem(new RunAsWork() - { - @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 beforeDeleteNode(final NodeRef nodeRef) - { - doAsSystem(new RunAsWork() - { - @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 doAsSystem(RunAsWork 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 NodeService nodeService; - private NodeRef folderNodeRef; - private Long minUid; - private Long maxUid; - private boolean forceNewUidValidity = false; - - public UidValidityTransactionListener(NodeRef folderNodeRef, NodeService nodeService) - { - this.folderNodeRef = folderNodeRef; - this.nodeService = nodeService; - } - - 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() - { - @Override - public Void doWork() throws Exception - { - if (UidValidityTransactionListener.this.forceNewUidValidity || UidValidityTransactionListener.this.minUid != null) - { - long modifDate = System.currentTimeMillis(); - Long oldMax = (Long)UidValidityTransactionListener.this.nodeService.getProperty(folderNodeRef, ImapModel.PROP_MAXUID); - // Only update UIDVALIDITY if a new node has and ID that is smaller than the old maximum (as UIDs are always meant to increase) - if (UidValidityTransactionListener.this.forceNewUidValidity || oldMax == null || UidValidityTransactionListener.this.minUid < oldMax) - { - UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate); - if (logger.isDebugEnabled()) - { - logger.debug("UIDVALIDITY was modified for folder, nodeRef:" + folderNodeRef); - } - } - if(UidValidityTransactionListener.this.maxUid != null) - { - UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_MAXUID, UidValidityTransactionListener.this.maxUid); - if (logger.isDebugEnabled()) - { - logger.debug("MAXUID was modified for folder, nodeRef:" + folderNodeRef); - } - } - } - UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_CHANGE_TOKEN, changeToken); - return null; - } - }); - } - } - - /** - * 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); - } - } - } - - 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; - } - - 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; - } - } -} +/* + * Copyright (C) 2005-2011 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 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.Arrays; +import java.util.Collection; +import java.util.Collections; +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.NavigableMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +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.AddressException; +import javax.mail.internet.ContentType; +import javax.mail.internet.InternetAddress; +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.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.SiteModel; +import org.alfresco.repo.site.SiteServiceException; +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.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +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.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.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.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.store.SimpleStoredMessage; + +/** + * @author Dmitry Vaserin + * @author Arseny Kovalchuk + * @author David Ward + * @since 3.2 + */ +public class ImapServiceImpl implements ImapService, 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 MimetypeService mimetypeService; + private NamespaceService namespaceService; + private SearchService searchService; + + // 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, FolderStatus> folderCache; + private int folderCacheSize = 1000; + private ReentrantReadWriteLock folderCacheLock = new ReentrantReadWriteLock(); + private SimpleCache messageCache; + private Map imapConfigMountPoints; + private Map mountPointIds; + 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) + { + service.startupInTxn(false); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @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 messageCache) + { + this.messageCache = messageCache; + } + + 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 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( + imapConfigMountPointsBeans.length * 2); + this.mountPointIds = new HashMap(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 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); + PropertyCheck.mandatory(this, "namespaceService", namespaceService); + PropertyCheck.mandatory(this, "searchService", getSearchService()); + this.folderCache = new MaxSizeMap, 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(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() + { + @Override + public Void doWork() throws Exception + { + List mailboxes = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback>() + { + @Override + public List execute() throws Throwable + { + startup(); + + List result = new LinkedList(); + + // 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)); + } + + // ---------------------- 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)) + { + fileFolderService.delete(fileInfo.getNodeRef()); + messageCache.remove(fileInfo.getNodeRef()); + } + } + + 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(), serviceRegistry); + } + final NodeRef root; + final List pathElements; + ImapViewMode viewMode = ImapViewMode.ARCHIVE; + int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + int mountPointId = 0; + if (index < 0) + { + root = getUserImapHomeRef(user.getLogin()); + pathElements = Collections.singletonList(mailboxName); + } + else + { + String rootPath = mailboxName.substring(0, index); + ImapConfigMountPointsBean imapConfigMountPoint = this.imapConfigMountPoints.get(rootPath); + if (imapConfigMountPoint != null) + { + mountPointId = this.mountPointIds.get(rootPath); + root = imapConfigMountPoint.getFolderPath(serviceRegistry.getNamespaceService(), nodeService, searchService, fileFolderService); + 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 + { + mailFolder = fileFolderService.resolveNamePath(root, pathElements, !mayCreate); + } + catch (FileNotFoundException e) + { + throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] + { + mailboxName + }); + } + if (mailFolder == null) + { + if (!mayCreate) + { + throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] + { + mailboxName + }); + } + if (logger.isDebugEnabled()) + { + logger.debug("Creating mailbox: " + mailboxName); + } + mailFolder = FileFolderUtil.makeFolders(fileFolderService, root, pathElements, ContentModel.TYPE_FOLDER); + } + else + { + if (!mayExist) + { + throw new AlfrescoRuntimeException(ERROR_FOLDER_ALREADY_EXISTS); + } + } + return new AlfrescoImapFolder(mailFolder, user.getLogin(), pathElements.get(pathElements.size() - 1), mailboxName, viewMode, + 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 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."); + } + } + } + + 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() + { + @Override + public String doWork() throws Exception + { + return (String) nodeService.getProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN); + } + }, AuthenticationUtil.getSystemUserName()); + + Pair cacheKey = null; + if (changeToken != null) + { + cacheKey = new Pair(userName, changeToken); + this.folderCacheLock.readLock().lock(); + try + { + FolderStatus result = this.folderCache.get(cacheKey); + if (result != null) + { + return result; + } + } + finally + { + this.folderCacheLock.readLock().unlock(); + } + } + + List fileInfos = null; + FileFilterMode.setClient(Client.imap); + try + { + fileInfos = fileFolderService.listFiles(contextNodeRef); + } + finally + { + FileFilterMode.clearClient(); + } + + final NavigableMap currentSearch = new TreeMap(); + + 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(userName, changeToken); + final String finalToken = changeToken; + doAsSystem(new RunAsWork() + { + @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); + 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 = getOrCreateMailbox(user, mailbox, true, false); + 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 Flags getFlags(FileInfo messageInfo) + { + Flags flags = new Flags(); + 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 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) + { + setFlag(messageInfo.getNodeRef(), flag, value); + } + + private void setFlag(NodeRef nodeRef, Flag flag, boolean value) + { + 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 + { + 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); + } + + /** + * Depend on listSubscribed param, list Mailboxes or list subscribed Mailboxes + */ + public 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(); + + // 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; + + 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) + { + String userName = user.getLogin(); + if (!listSubscribed || isSubscribed(mountPointFileInfo, userName)) + { + result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, + isExtractionEnabled(mountPointFileInfo.getNodeRef()), serviceRegistry, mountPointId)); + } + else if (rootPath.endsWith("%") && !expandFolder(mountPoint, user, mountPointName, "%", true, viewMode, mountPointId).isEmpty()) // \NoSelect + { + result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, + 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 + */ + private List 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 fullList = new LinkedList(); + ImapSubFolderFilter filter = new ImapSubFolderFilter(viewMode, name.replace('%', '*')); + List 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 + { + 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.emptyList() : Collections.singletonList(fileInfo); + } + + if (index < 0) + { + // This is the last level + for (FileInfo fileInfo : list) + { + if (!filter.isEnterSubfolder(fileInfo.getNodeRef())) + { + continue; + } + String folderPath = rootPathPrefix + fileInfo.getName(); + String userName = user.getLogin(); + if (!listSubscribed || isSubscribed(fileInfo, userName)) + { + fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode, + isExtractionEnabled(fileInfo.getNodeRef()), 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, + 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() + { + 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); + } + 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() + { + 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); + } + + 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); + + NodeRef rootNode = nodeService.getRootNode(storeRef); + + List 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 typesToExclude; + private List 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, nodeService); + AlfrescoTransactionSupport.bindListener(txnListener); + AlfrescoTransactionSupport.bindResource(key, txnListener); + } + return txnListener; + } + + @Override + public void onCreateChildAssociation(final ChildAssociationRef childAssocRef, boolean isNewNode) + { + doAsSystem(new RunAsWork() + { + @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() + { + @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 before, + final Map after) + { + doAsSystem(new RunAsWork() + { + @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 beforeDeleteNode(final NodeRef nodeRef) + { + doAsSystem(new RunAsWork() + { + @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 doAsSystem(RunAsWork 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 NodeService nodeService; + private NodeRef folderNodeRef; + private Long minUid; + private Long maxUid; + private boolean forceNewUidValidity = false; + + public UidValidityTransactionListener(NodeRef folderNodeRef, NodeService nodeService) + { + this.folderNodeRef = folderNodeRef; + this.nodeService = nodeService; + } + + 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() + { + @Override + public Void doWork() throws Exception + { + if (UidValidityTransactionListener.this.forceNewUidValidity || UidValidityTransactionListener.this.minUid != null) + { + long modifDate = System.currentTimeMillis(); + Long oldMax = (Long)UidValidityTransactionListener.this.nodeService.getProperty(folderNodeRef, ImapModel.PROP_MAXUID); + // Only update UIDVALIDITY if a new node has and ID that is smaller than the old maximum (as UIDs are always meant to increase) + if (UidValidityTransactionListener.this.forceNewUidValidity || oldMax == null || UidValidityTransactionListener.this.minUid < oldMax) + { + UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate); + if (logger.isDebugEnabled()) + { + logger.debug("UIDVALIDITY was modified for folder, nodeRef:" + folderNodeRef); + } + } + if(UidValidityTransactionListener.this.maxUid != null) + { + UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_MAXUID, UidValidityTransactionListener.this.maxUid); + if (logger.isDebugEnabled()) + { + logger.debug("MAXUID was modified for folder, nodeRef:" + folderNodeRef); + } + } + } + UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_CHANGE_TOKEN, changeToken); + return null; + } + }); + } + } + + /** + * 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); + } + } + } + + 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; + } + + 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; + } + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/importer/ImporterComponent.java b/source/java/org/alfresco/repo/importer/ImporterComponent.java index 9422da1089..ab05b7215f 100644 --- a/source/java/org/alfresco/repo/importer/ImporterComponent.java +++ b/source/java/org/alfresco/repo/importer/ImporterComponent.java @@ -33,6 +33,7 @@ import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.importer.view.NodeContext; +import org.alfresco.repo.model.filefolder.HiddenAspect; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.security.authentication.AuthenticationContext; import org.alfresco.repo.version.Version2Model; @@ -108,6 +109,7 @@ public class ImporterComponent private AuthenticationContext authenticationContext; private OwnableService ownableService; private VersionService versionService; + private HiddenAspect hiddenAspect; /** * The db node service, used when updating the version store. @@ -236,6 +238,11 @@ public class ImporterComponent this.dbNodeService = nodeService; } + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.hiddenAspect = hiddenAspect; + } + /* (non-Javadoc) * @see org.alfresco.service.cmr.view.ImporterService#importView(java.io.InputStreamReader, org.alfresco.service.cmr.view.Location, java.util.Properties, org.alfresco.service.cmr.view.ImporterProgress) */ @@ -611,6 +618,9 @@ public class ImporterComponent } } + // check whether the node should be hidden + hiddenAspect.checkHidden(nodeRef); + // import content, if applicable for (Map.Entry property : context.getProperties().entrySet()) { diff --git a/source/java/org/alfresco/repo/jscript/ScriptNode.java b/source/java/org/alfresco/repo/jscript/ScriptNode.java index e4e8bd4361..f555ec67d8 100644 --- a/source/java/org/alfresco/repo/jscript/ScriptNode.java +++ b/source/java/org/alfresco/repo/jscript/ScriptNode.java @@ -92,6 +92,8 @@ import org.alfresco.service.namespace.NamespacePrefixResolverProvider; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.GUID; import org.alfresco.util.ISO9075; import org.alfresco.util.Pair; @@ -649,10 +651,18 @@ public class ScriptNode implements Scopeable, NamespacePrefixResolverProvider PagingRequest pageRequest = new PagingRequest(skipOffset, maxItems, queryExecutionId); pageRequest.setRequestTotalCountMax(requestTotalCountMax); - PagingResults pageOfNodeInfos = this.fileFolderService.list(this.nodeRef, files, folders, null, ignoreTypeQNames, sortProps, pageRequest); - + PagingResults pageOfNodeInfos = null; + FileFilterMode.setClient(Client.script); + try + { + pageOfNodeInfos = this.fileFolderService.list(this.nodeRef, files, folders, null, ignoreTypeQNames, sortProps, pageRequest); + } + finally + { + FileFilterMode.clearClient(); + } + List nodeInfos = pageOfNodeInfos.getPage(); - int size = nodeInfos.size(); results = new Object[size]; for (int i=0; i toFileInfo(List nodeRefs) throws InvalidTypeException { List results = new ArrayList(nodeRefs.size()); + Client client = FileFilterMode.getClient(); for (NodeRef nodeRef : nodeRefs) { try { + if(hiddenAspect.getVisibility(client, nodeRef) == Visibility.NotVisible) + { + continue; + } FileInfo fileInfo = toFileInfo(nodeRef, true); results.add(fileInfo); } @@ -257,12 +271,13 @@ public class FileFolderServiceImpl implements FileFolderService QName typeQName = nodeService.getType(nodeRef); boolean isFolder = isFolder(typeQName); boolean isHidden = false; - - if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_HIDDEN)) + + Client client = FileFilterMode.getClient(); + if(hiddenAspect.getVisibility(client, nodeRef) == Visibility.HiddenAttribute) { - isHidden = true; + isHidden = true; } - + // Construct the file info and add to the results FileInfo fileInfo = new FileInfoImpl(nodeRef, typeQName, isFolder, isHidden, properties); @@ -380,8 +395,14 @@ public class FileFolderServiceImpl implements FileFolderService } final List nodeInfos = new ArrayList(nodeRefs.size()); + final Client client = FileFilterMode.getClient(); for (NodeRef nodeRef : nodeRefs) { + if(hiddenAspect.getVisibility(client, nodeRef) == Visibility.NotVisible) + { + continue; + } + nodeInfos.add(toFileInfo(nodeRef, true)); } PermissionCheckedCollectionMixin.create(nodeInfos, nodeRefs); @@ -1208,6 +1229,7 @@ public class FileFolderServiceImpl implements FileFolderService } NodeRef nodeRef = assocRef.getChildRef(); + FileInfo fileInfo = toFileInfo(nodeRef, true); // done if (logger.isDebugEnabled()) @@ -1441,19 +1463,4 @@ public class FileFolderServiceImpl implements FileFolderService } return new Pair(base, ext); } - - public List removeHiddenFiles(List files) - { - List ret = new ArrayList(files.size()); - - for(FileInfo file : files) - { - if(!nodeService.hasAspect(file.getNodeRef(), ContentModel.ASPECT_HIDDEN)) - { - ret.add(file); - } - } - - return ret; - } } diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java index f2ce806560..0152c41319 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java @@ -33,7 +33,6 @@ import javax.transaction.UserTransaction; import junit.framework.TestCase; import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.jlan.server.FileFilterMode; import org.alfresco.model.ContentModel; import org.alfresco.model.ForumModel; import org.alfresco.query.PagingRequest; @@ -43,6 +42,7 @@ import org.alfresco.repo.dictionary.DictionaryDAO; import org.alfresco.repo.dictionary.M2Model; import org.alfresco.repo.dictionary.M2Type; import org.alfresco.repo.model.filefolder.FileFolderServiceImpl.InvalidTypeException; +import org.alfresco.repo.model.filefolder.HiddenAspect.Visibility; import org.alfresco.repo.node.integrity.IntegrityChecker; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; @@ -69,6 +69,8 @@ import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; import org.alfresco.util.GUID; import org.springframework.context.ApplicationContext; import org.springframework.extensions.surf.util.I18NUtil; @@ -97,6 +99,7 @@ public class FileFolderServiceImplTest extends TestCase private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private HiddenAspect hiddenAspect; private TransactionService transactionService; private NodeService nodeService; private FileFolderService fileFolderService; @@ -117,6 +120,7 @@ public class FileFolderServiceImplTest extends TestCase permissionService = serviceRegistry.getPermissionService(); authenticationService = (MutableAuthenticationService) ctx.getBean("AuthenticationService"); dictionaryDAO = (DictionaryDAO) ctx.getBean("dictionaryDAO"); + hiddenAspect = (HiddenAspect)ctx.getBean("hiddenAspect"); // start the transaction txn = transactionService.getUserTransaction(); @@ -1280,31 +1284,74 @@ public class FileFolderServiceImplTest extends TestCase public void testHiddenFiles() { - FileFilterMode.setMode(FileFilterMode.Mode.ENHANCED); + FileFilterMode.setClient(Client.webdav); - NodeRef parent = fileFolderService.create(rootNodeRef, "New Folder", ContentModel.TYPE_FOLDER).getNodeRef(); - NodeRef child = fileFolderService.create(parent, "file.tmp", ContentModel.TYPE_CONTENT).getNodeRef(); - assertTrue(nodeService.hasAspect(child, ContentModel.ASPECT_TEMPORARY)); - assertTrue(!nodeService.hasAspect(child, ContentModel.ASPECT_HIDDEN)); + try + { + NodeRef parent = fileFolderService.create(rootNodeRef, "New Folder", ContentModel.TYPE_FOLDER).getNodeRef(); + NodeRef child = fileFolderService.create(parent, "file.tmp", ContentModel.TYPE_CONTENT).getNodeRef(); + assertTrue(nodeService.hasAspect(child, ContentModel.ASPECT_TEMPORARY)); + assertTrue(!nodeService.hasAspect(child, ContentModel.ASPECT_HIDDEN)); + + NodeRef parent1 = fileFolderService.create(rootNodeRef, ".TemporaryItems", ContentModel.TYPE_FOLDER).getNodeRef(); + NodeRef child1 = fileFolderService.create(parent1, "file1", ContentModel.TYPE_CONTENT).getNodeRef(); + assertTrue(nodeService.hasAspect(child1, ContentModel.ASPECT_TEMPORARY)); + assertTrue(nodeService.hasAspect(child1, ContentModel.ASPECT_HIDDEN)); + + NodeRef parent2 = fileFolderService.create(rootNodeRef, "Folder 2", ContentModel.TYPE_FOLDER).getNodeRef(); + NodeRef child2 = fileFolderService.create(parent2, "Thumbs.db", ContentModel.TYPE_CONTENT).getNodeRef(); + assertTrue(!nodeService.hasAspect(child2, ContentModel.ASPECT_TEMPORARY)); + assertTrue(nodeService.hasAspect(child2, ContentModel.ASPECT_HIDDEN)); + // set hidden attribute for cifs, webdav should be able to see, other clients not + assertEquals(Visibility.Visible, hiddenAspect.getVisibility(Client.webdav, child2)); + assertEquals(Visibility.HiddenAttribute, hiddenAspect.getVisibility(Client.cifs, child2)); + assertEquals(Visibility.NotVisible, hiddenAspect.getVisibility(Client.script, child2)); + assertEquals(Visibility.NotVisible, hiddenAspect.getVisibility(Client.webclient, child2)); - NodeRef parent1 = fileFolderService.create(rootNodeRef, ".TemporaryItems", ContentModel.TYPE_FOLDER).getNodeRef(); - NodeRef child1 = fileFolderService.create(parent1, "file1", ContentModel.TYPE_CONTENT).getNodeRef(); - assertTrue(nodeService.hasAspect(child1, ContentModel.ASPECT_TEMPORARY)); - assertTrue(nodeService.hasAspect(child1, ContentModel.ASPECT_HIDDEN)); + NodeRef node1 = fileFolderService.create(rootNodeRef, "surf-config", ContentModel.TYPE_FOLDER).getNodeRef(); + assertTrue(nodeService.hasAspect(node1, ContentModel.ASPECT_HIDDEN)); + // surf-config should not be visible to any client + for(Client client : hiddenAspect.getClients()) + { + assertEquals(Visibility.NotVisible, hiddenAspect.getVisibility(client, node1)); + } + + NodeRef node2 = fileFolderService.create(rootNodeRef, ".DS_Store", ContentModel.TYPE_CONTENT).getNodeRef(); + assertTrue(nodeService.hasAspect(node2, ContentModel.ASPECT_HIDDEN)); + // .DS_Store is a system path and so is visible in nfs and webdav, as a hidden file in cifs and hidden to all other clients + for(Client client : hiddenAspect.getClients()) + { + if(client == Client.cifs) + { + assertEquals(Visibility.HiddenAttribute, hiddenAspect.getVisibility(client, node2)); + } + else if(client == Client.webdav) + { + assertEquals(Visibility.Visible, hiddenAspect.getVisibility(client, node2)); + } + else if(client == Client.nfs) + { + assertEquals(Visibility.Visible, hiddenAspect.getVisibility(client, node2)); + } + else + { + assertEquals(Visibility.NotVisible, hiddenAspect.getVisibility(client, node2)); + } + } - NodeRef parent2 = fileFolderService.create(rootNodeRef, "Folder 2", ContentModel.TYPE_FOLDER).getNodeRef(); - NodeRef child2 = fileFolderService.create(parent2, "Thumbs.db", ContentModel.TYPE_CONTENT).getNodeRef(); - assertTrue(!nodeService.hasAspect(child2, ContentModel.ASPECT_TEMPORARY)); - assertTrue(nodeService.hasAspect(child2, ContentModel.ASPECT_HIDDEN)); - - List children = fileFolderService.list(parent); - assertEquals(1, children.size()); - - children = fileFolderService.list(parent1); - assertEquals(1, children.size()); - - children = fileFolderService.list(parent2); - assertEquals(1, children.size()); + List children = fileFolderService.list(parent); + assertEquals(1, children.size()); + + children = fileFolderService.list(parent1); + assertEquals(1, children.size()); + + children = fileFolderService.list(parent2); + assertEquals(1, children.size()); + } + finally + { + FileFilterMode.clearClient(); + } } public void testPatterns() diff --git a/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java index af7eb7ea93..e3d8448466 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileInfoImpl.java @@ -42,7 +42,7 @@ public class FileInfoImpl implements FileInfo private NodeRef linkNodeRef; private boolean isFolder; private boolean isLink; - private boolean isHindden; + private boolean isHidden; private Map properties; private QName typeQName; @@ -61,7 +61,7 @@ public class FileInfoImpl implements FileInfo this.isFolder = isFolder; this.properties = properties; - this.isHindden = isHidden; + this.isHidden = isHidden; // Check if this is a link node if ( properties.containsKey( ContentModel.PROP_LINK_DESTINATION)) @@ -94,6 +94,11 @@ public class FileInfoImpl implements FileInfo return (this.getNodeRef().equals(that.getNodeRef())); } + void setHidden(boolean isHidden) + { + this.isHidden = isHidden; + } + /** * @see #getNodeRef() * @see NodeRef#hashCode() @@ -139,7 +144,7 @@ public class FileInfoImpl implements FileInfo } public boolean isHidden() { - return isHindden; + return isHidden; } public NodeRef getLinkNodeRef() diff --git a/source/java/org/alfresco/repo/model/filefolder/FilenameFilteringInterceptor.java b/source/java/org/alfresco/repo/model/filefolder/FilenameFilteringInterceptor.java index a823e58e07..caefc1d8be 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FilenameFilteringInterceptor.java +++ b/source/java/org/alfresco/repo/model/filefolder/FilenameFilteringInterceptor.java @@ -17,14 +17,10 @@ * along with Alfresco. If not, see . */ package org.alfresco.repo.model.filefolder; -import java.io.Serializable; -import java.util.HashMap; import java.util.Iterator; -import java.util.Map; -import org.alfresco.jlan.server.FileFilterMode; -import org.alfresco.jlan.server.FileFilterMode.Mode; import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.filefolder.HiddenAspect.Visibility; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.NodeRef; @@ -33,6 +29,9 @@ import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.repository.Path.Element; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; +import org.alfresco.util.FileFilterMode.Mode; import org.alfresco.util.PatternFilter; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -40,8 +39,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** - * An interceptor that intercepts the FileFolderService create method, creating files and folders as the system user - * and applying temporary and hidden aspects when they match a pattern. + * An interceptor that intercepts FileFolderService methods, ensuring system, temporary and hidden files + * and paths are marked with the correct aspects. * */ public class FilenameFilteringInterceptor implements MethodInterceptor @@ -52,22 +51,13 @@ public class FilenameFilteringInterceptor implements MethodInterceptor private PermissionService permissionService; private PatternFilter temporaryFiles; - private PatternFilter hiddenFiles; private PatternFilter systemPaths; + private HiddenAspect hiddenAspect; public FilenameFilteringInterceptor() { } - /** - * A list of regular expressions that represent patterns of hidden files. - * - */ - public void setHiddenFiles(PatternFilter hiddenFiles) - { - this.hiddenFiles = hiddenFiles; - } - /** * A list of regular expressions that represent patterns of temporary files. * @@ -77,7 +67,12 @@ public class FilenameFilteringInterceptor implements MethodInterceptor this.temporaryFiles = temporaryFiles; } - /** + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.hiddenAspect = hiddenAspect; + } + + /** * A list of regular expressions that represent patterns of system paths. * */ @@ -90,6 +85,11 @@ public class FilenameFilteringInterceptor implements MethodInterceptor { return FileFilterMode.getMode(); } + + public Client getClient() + { + return FileFilterMode.getClient(); + } /** * @param nodeService the service to use to apply the sys:temporary aspect @@ -134,49 +134,6 @@ public class FilenameFilteringInterceptor implements MethodInterceptor } } } - - private void checkHiddenAspect(boolean isHidden, FileInfo fileInfo) - { - NodeRef nodeRef = fileInfo.getNodeRef(); - - if (isHidden) - { - nodeService.addAspect(nodeRef, ContentModel.ASPECT_HIDDEN, null); - - Map props = new HashMap(2); - props.put(ContentModel.PROP_IS_INDEXED, Boolean.FALSE); - props.put(ContentModel.PROP_IS_CONTENT_INDEXED, Boolean.FALSE); - nodeService.addAspect(nodeRef, ContentModel.ASPECT_INDEX_CONTROL, props); - - if (logger.isDebugEnabled()) - { - logger.debug("Applied hidden marker: " + fileInfo); - } - } - else - { - if(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_HIDDEN)) - { - // Remove the aspect - nodeService.removeAspect(nodeRef, ContentModel.ASPECT_HIDDEN); - - if (logger.isDebugEnabled()) - { - logger.debug("Removed hidden marker: " + fileInfo); - } - } - } - } - - private void addIndexedAspect(FileInfo fileInfo) - { - NodeRef nodeRef = fileInfo.getNodeRef(); - - Map props = new HashMap(2); - props.put(ContentModel.PROP_IS_INDEXED, Boolean.FALSE); - props.put(ContentModel.PROP_IS_CONTENT_INDEXED, Boolean.FALSE); - nodeService.addAspect(nodeRef, ContentModel.ASPECT_INDEX_CONTROL, props); - } private Object runAsSystem(MethodInvocation invocation) throws Throwable { @@ -219,6 +176,15 @@ public class FilenameFilteringInterceptor implements MethodInterceptor return ret; } + private int getSystemFileVisibilityMask() + { + int mask = 0; + mask |= hiddenAspect.getClientVisibilityMask(Client.cifs, Visibility.HiddenAttribute); + mask |= hiddenAspect.getClientVisibilityMask(Client.webdav, Visibility.Visible); + mask |= hiddenAspect.getClientVisibilityMask(Client.nfs, Visibility.Visible); + return mask; + } + public Object invoke(final MethodInvocation invocation) throws Throwable { // execute and get the result @@ -237,34 +203,32 @@ public class FilenameFilteringInterceptor implements MethodInterceptor { // it's a system file/folder, create as system and allow full control to all authorities ret = runAsSystem(invocation); - permissionService.setPermission(((FileInfo)ret).getNodeRef(), PermissionService.ALL_AUTHORITIES, PermissionService.FULL_CONTROL, true); + FileInfoImpl fileInfo = (FileInfoImpl)ret; + permissionService.setPermission(fileInfo.getNodeRef(), PermissionService.ALL_AUTHORITIES, PermissionService.FULL_CONTROL, true); // it's always marked temporary and hidden - checkTemporaryAspect(true, (FileInfo)ret); - checkHiddenAspect(true, (FileInfo)ret); - addIndexedAspect((FileInfo)ret); + checkTemporaryAspect(true, fileInfo); + hiddenAspect.hideNode(fileInfo, getSystemFileVisibilityMask()); } else { // it's not a temporary file/folder, create as normal ret = invocation.proceed(); + + FileInfoImpl fileInfo = (FileInfoImpl)ret; +// NodeRef retNodeRef = fileInfo.getNodeRef(); - // if it's on a temporary path check whether temporary and hidden aspects need to be applied if(isSystemPath(nodeRef, filename)) { - checkTemporaryAspect(true, (FileInfo)ret); - checkHiddenAspect(true, (FileInfo)ret); - addIndexedAspect((FileInfo)ret); + // it's on a system path, check whether temporary, hidden and noindex aspects need to be applied + checkTemporaryAspect(true, fileInfo); + hiddenAspect.hideNode(fileInfo, getSystemFileVisibilityMask()); } else { + // check whether it's a temporary or hidden file checkTemporaryAspect(temporaryFiles.isFiltered(filename), (FileInfo)ret); - boolean isHidden = hiddenFiles.isFiltered(filename); - checkHiddenAspect(isHidden, (FileInfo)ret); - if(isHidden) - { - addIndexedAspect((FileInfo)ret); - } + hiddenAspect.checkHidden(fileInfo); } } } @@ -272,7 +236,11 @@ public class FilenameFilteringInterceptor implements MethodInterceptor { ret = invocation.proceed(); - checkTemporaryAspect(temporaryFiles.isFiltered(filename), (FileInfo)ret); + FileInfoImpl fileInfo = (FileInfoImpl)ret; + //NodeRef retNodeRef = fileInfo.getNodeRef(); + + checkTemporaryAspect(temporaryFiles.isFiltered(filename), fileInfo); + hiddenAspect.checkHidden(fileInfo); } } else if (methodName.startsWith("rename") || @@ -281,23 +249,19 @@ public class FilenameFilteringInterceptor implements MethodInterceptor { ret = invocation.proceed(); - FileInfo fileInfo = (FileInfo) ret; + FileInfoImpl fileInfo = (FileInfoImpl) ret; String filename = fileInfo.getName(); - +// NodeRef retNodeRef = fileInfo.getNodeRef(); + if (logger.isDebugEnabled()) { logger.debug("Checking filename returned by " + methodName + ": " + filename); } - + // check against all the regular expressions checkTemporaryAspect(temporaryFiles.isFiltered(filename), fileInfo); - - boolean isHidden = hiddenFiles.isFiltered(filename); - checkHiddenAspect(isHidden, fileInfo); - if(isHidden) - { - addIndexedAspect((FileInfo)ret); - } + hiddenAspect.checkHidden(fileInfo); +// hiddenAspect.checkHidden(retNodeRef); } else { diff --git a/source/java/org/alfresco/repo/model/filefolder/HiddenAspect.java b/source/java/org/alfresco/repo/model/filefolder/HiddenAspect.java new file mode 100644 index 0000000000..762aedb059 --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/HiddenAspect.java @@ -0,0 +1,454 @@ +package org.alfresco.repo.model.filefolder; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.Path.Element; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Functionality relating to hidden files and folders. + * + * Support for nodes marked as hidden but with visibility constraints for specific clients. A node + * can have the hidden aspect applied, which means that the node is hidden. However, + * for specific clients it can be defined whether the node is visible or will have its hidden attribute + * set in FileInfo. + * + */ +public class HiddenAspect +{ + private static Log logger = LogFactory.getLog(HiddenAspect.class); + + public static enum Visibility + { + NotVisible, Visible, HiddenAttribute; + + public int getMask() + { + if(this == Visible) + { + return 2; + } + else if(this == HiddenAttribute) + { + return 1; + } + else if(this == NotVisible) + { + return 0; + } + else + { + throw new IllegalArgumentException(); + } + } + + public static Visibility getVisibility(int mask) + { + if(mask == 2) + { + return Visible; + } + else if(mask == 1) + { + return HiddenAttribute; + } + else if(mask == 0) + { + return NotVisible; + } + else + { + throw new IllegalArgumentException(); + } + } + }; + + private List filters = new ArrayList(10); + + private NodeService nodeService; + + public HiddenAspect() + { + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPatterns(List filters) + { + for(HiddenFileFilter filter : filters) + { + this.filters.add(new HiddenFileInfoImpl(filter.getFilter(), filter.getVisibility(), filter.getHiddenAttribute())); + } + } + + public Client[] getClients() + { + return Client.values(); + } + + private Integer getClientIndex(Client client) + { + return client.ordinal(); + } + + private void addIndexControlAspect(NodeRef nodeRef) + { + Map props = new HashMap(2); + props.put(ContentModel.PROP_IS_INDEXED, Boolean.FALSE); + props.put(ContentModel.PROP_IS_CONTENT_INDEXED, Boolean.FALSE); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_INDEX_CONTROL, props); + + if (logger.isDebugEnabled()) + { + logger.debug("Applied index control marker: " + nodeRef); + } + } + + private void addHiddenAspect(NodeRef nodeRef, int visibilityMask) + { + Map props = new HashMap(1); + props.put(ContentModel.PROP_VISIBILITY_MASK, visibilityMask); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_HIDDEN, props); + + if (logger.isDebugEnabled()) + { + logger.debug("Applied hidden marker: " + nodeRef); + } + } + + private void removeHiddenAspect(NodeRef nodeRef) + { + // Remove the aspect + nodeService.removeAspect(nodeRef, ContentModel.ASPECT_HIDDEN); + + if (logger.isDebugEnabled()) + { + logger.debug("Removed hidden marker: " + nodeRef); + } + } + + private Visibility getVisibility(Integer mask, Client client) + { + if(mask == null || mask.intValue() == 0) + { + return Visibility.NotVisible; + } + + mask = (mask.intValue() >> (getClientIndex(client))*2) & 3; + + return Visibility.getVisibility(mask); + } + + /** + * Determines whether the path matches any one of the hidden file patterns and, if so, + * returns the matching pattern. + * + * @param path + * @return + */ + private HiddenFileInfo isHidden(String path) + { + // check against all the filters + HiddenFileInfoImpl matched = null; + + for(HiddenFileInfoImpl filter : filters) + { + if(filter.isHidden(path)) + { + matched = filter; + break; + } + } + + return matched; + } + + public int getClientVisibilityMask(Client client, Visibility visibility) + { + return visibility.getMask() << getClientIndex(client)*2; + } + + /** + * Checks whether the node is on a hidden path + * + * @param nodeRef + * @return the matching filter, or null if no match + */ + public HiddenFileInfo isHidden(NodeRef nodeRef) + { + HiddenFileInfo ret = null; + // TODO would be nice to check each part of the path in turn, bailing out if a match is found + Path path = nodeService.getPath(nodeRef); + + Iterator it = path.iterator(); + while(it.hasNext()) + { + Path.ChildAssocElement elem = (Path.ChildAssocElement)it.next(); + QName qname = elem.getRef().getQName(); + if(qname != null) + { + ret = isHidden(qname.getLocalName()); + if(ret != null) + { + break; + } + } + } + + return ret; + } + + /** + * Hides the node by applying the hidden and not indexed aspects. The node will be hidden from all clients. + * + * @param client + * @param fileInfo + * @return + */ + public void hideNode(NodeRef nodeRef) + { + addHiddenAspect(nodeRef, 0); + addIndexControlAspect(nodeRef); + } + + /** + * Hides the node by applying the hidden and not indexed aspects. The node will be hidden from clients + * according to the visibility mask. + * + * @param client + * @param fileInfo + * @return + */ + public void hideNode(NodeRef nodeRef, int clientVisibilityMask) + { + addHiddenAspect(nodeRef, clientVisibilityMask); + addIndexControlAspect(nodeRef); + } + + public void checkHidden(FileInfoImpl fileInfo) + { + NodeRef nodeRef = fileInfo.getNodeRef(); + HiddenFileInfo hiddenFileInfo = checkHidden(nodeRef); + if(hiddenFileInfo != null) + { + fileInfo.setHidden(true); + } + } + + public void hideNode(FileInfoImpl fileInfo, int visibilityMask) + { + hideNode(fileInfo.getNodeRef(), visibilityMask); + fileInfo.setHidden(true); + } + + /** + * Checks whether the file should be hidden and applies the hidden and not indexed aspects if so. + * + * @param client + * @param fileInfo + * @return + */ + public HiddenFileInfo checkHidden(NodeRef nodeRef) + { + HiddenFileInfo filter = isHidden(nodeRef); + if(filter != null) + { + // the file matches a pattern, apply the hidden and aspect control aspects + addHiddenAspect(nodeRef, filter.getVisibilityMask()); + addIndexControlAspect(nodeRef); + } + else + { + // the file does not match the pattern, ensure that the hidden aspect is not present + if(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_HIDDEN)) + { + removeHiddenAspect(nodeRef); + } + } + + return filter; + } + + /** + * Gets the visibility constraint for the given client on the given node. + * + * @param client + * @param nodeRef + * + * @return the visibility constraint for the given client and node + */ + public Visibility getVisibility(Client client, NodeRef nodeRef) + { + Visibility ret = null; + + if(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_HIDDEN)) + { + Integer visibilityMask = (Integer)nodeService.getProperty(nodeRef, ContentModel.PROP_VISIBILITY_MASK); + if(visibilityMask != null) + { + if(visibilityMask.intValue() == 0) + { + ret = Visibility.NotVisible; + } + else if(client == null) + { + ret = Visibility.NotVisible; + } + else + { + ret = getVisibility(visibilityMask.intValue(), client); + } + } + else + { + // no visibility mask property, so retain backwards compatibility with 3.4 hidden aspect behaviour + if(client == Client.cifs) + { + ret = Visibility.HiddenAttribute; + } + else if(client == Client.webdav || client == Client.nfs || client == Client.imap) + { + ret = Visibility.Visible; + } + else + { + ret = Visibility.NotVisible; + } + } + } + + return ret; + } + + /** + * Return the list of files with hidden files filtered out if required for the given client. + * + * @param client + * @param files + * @return + */ +// public List removeHiddenFiles(Client client, List files) +// { +// // TODO bulk load aspects and properties of nodes first? +// List ret = new ArrayList(files.size()); +// int numHiddenFiles = 0; +// +// for(FileInfo file : files) +// { +// if(getVisibility(client, file.getNodeRef()) == Visibility.NotVisible) +// { +// numHiddenFiles++; +// continue; +// } +// +// ret.add(file); +// } +// +// return ret; +// } + + private class HiddenFileInfoImpl implements HiddenFileInfo + { + private Pattern filter; + private Set clientVisibility = new HashSet(10); + private Set hiddenAttribute = new HashSet(10); + private int visibilityMask; + + public HiddenFileInfoImpl(String regexp, String visibility, String hiddenAttribute) + { + this.filter = Pattern.compile(regexp); + setVisibility(visibility); + setHiddenAttribute(hiddenAttribute); + calculateVisibilityMask(); + } + + private void setVisibility(String visibility) + { + if(visibility != null && !visibility.equals("")) + { + for(String clientStr : visibility.split(",")) + { + Client client = Client.getClient(clientStr); + this.clientVisibility.add(client); + } + } + } + + private void setHiddenAttribute(String hiddenAttribute) + { + if(hiddenAttribute != null && !hiddenAttribute.equals("")) + { + for(String clientStr : hiddenAttribute.split(",")) + { + Client client = Client.getClient(clientStr); + this.hiddenAttribute.add(client); + } + } + } + + private void calculateVisibilityMask() + { + visibilityMask = 0; + for(Client client : getClients()) + { + if(clientVisibility.contains(client)) + { + visibilityMask |= getClientVisibilityMask(client, Visibility.Visible); + } + else if(hiddenAttribute.contains(client)) + { + visibilityMask |= getClientVisibilityMask(client, Visibility.HiddenAttribute); + } + else + { + visibilityMask |= getClientVisibilityMask(client, Visibility.NotVisible); + } + } + } + + public String getFilter() + { + return filter.pattern(); + } + + public Set getVisibility() + { + return clientVisibility; + } + + public Set getHiddenAttribute() + { + return hiddenAttribute; + } + + public int getVisibilityMask() + { + return visibilityMask; + } + + boolean isHidden(String path) + { + return filter.matcher(path).matches(); + } + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/HiddenFileFilter.java b/source/java/org/alfresco/repo/model/filefolder/HiddenFileFilter.java new file mode 100644 index 0000000000..4f20f25043 --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/HiddenFileFilter.java @@ -0,0 +1,62 @@ +package org.alfresco.repo.model.filefolder; + +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +public class HiddenFileFilter implements InitializingBean +{ + private String filter; + private String visibility; + private String hiddenAttribute; + + public HiddenFileFilter() + { + } + + public void setFilter(String filter) + { + this.filter = filter; + } + + public void setVisibility(String visibility) + { + this.visibility = visibility; + } + + public String getFilter() + { + return filter; + } + + public String getVisibility() + { + return visibility; + } + + public String getHiddenAttribute() + { + return hiddenAttribute; + } + + public void setHiddenAttribute(String hiddenAttribute) + { + this.hiddenAttribute = hiddenAttribute; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "filter", filter); + if(visibility == null) + { + visibility = ""; + } + if(hiddenAttribute == null) + { + hiddenAttribute = ""; + } + } +} diff --git a/source/java/org/alfresco/repo/model/filefolder/HiddenFileInfo.java b/source/java/org/alfresco/repo/model/filefolder/HiddenFileInfo.java new file mode 100644 index 0000000000..11a422794b --- /dev/null +++ b/source/java/org/alfresco/repo/model/filefolder/HiddenFileInfo.java @@ -0,0 +1,12 @@ +package org.alfresco.repo.model.filefolder; + +import java.util.Set; + +import org.alfresco.util.FileFilterMode.Client; + +public interface HiddenFileInfo +{ + public Set getVisibility(); + public int getVisibilityMask(); + public String getFilter(); +} diff --git a/source/java/org/alfresco/service/cmr/model/FileFolderService.java b/source/java/org/alfresco/service/cmr/model/FileFolderService.java index a3395905fd..1b5be144ba 100644 --- a/source/java/org/alfresco/service/cmr/model/FileFolderService.java +++ b/source/java/org/alfresco/service/cmr/model/FileFolderService.java @@ -396,12 +396,4 @@ public interface FileFolderService */ @Auditable(parameters = {"typeQName"}) public FileFolderServiceType getType(QName typeQName); - - /** - * Removes any hidden files from the file list. - * - * @param files - * @return a list of files with hidden files removed - */ - public List removeHiddenFiles(List files); }