diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 9d4e82b90a..760c0c7523 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -163,6 +163,10 @@ + + + + - + true diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTest.java b/source/java/org/alfresco/repo/avm/AVMServiceTest.java index 48a2867511..5d1b7b1e64 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTest.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -163,7 +163,202 @@ public class AVMServiceTest extends AVMServiceTestBase } } - + + private enum DiffActionEnum + { + CREATION, MODIFICATION, DELETION, DELETION_AND_MODIFICATION + } + + /** + * Test is related to ALF-4098 + * + * @throws Exception + */ + public void testDiffOfNewItems() throws Exception + { + try + { + performeDiffTesting(DiffActionEnum.CREATION); + } + finally + { + fService.purgeStore("testStore"); + fService.purgeStore("submitStore"); + } + } + + /** + * Test is related to ALF-4098 + * + * @throws Exception + */ + public void testDiffOfModifiedItems() throws Exception + { + try + { + performeDiffTesting(DiffActionEnum.MODIFICATION); + } + finally + { + fService.purgeStore("testStore"); + fService.purgeStore("submitStore"); + } + } + + /** + * Test is related to ALF-4098 + * + * @throws Exception + */ + public void testDiffOfDeletedItems() throws Exception + { + try + { + performeDiffTesting(DiffActionEnum.DELETION); + } + finally + { + fService.purgeStore("testStore"); + fService.purgeStore("submitStore"); + } + } + + public void testDiffOfDeletedItemsInModifiedDirectory() throws Exception + { + try + { + performeDiffTesting(DiffActionEnum.DELETION_AND_MODIFICATION); + } + finally + { + fService.purgeStore("testStore"); + fService.purgeStore("submitStore"); + } + } + + private void performeDiffTesting(DiffActionEnum action) throws IOException + { + fService.createStore("testStore"); + fService.createStore("submitStore"); + + fService.createDirectory("submitStore:/", "root"); + fService.createLayeredDirectory("submitStore:/root", "testStore:/", "root"); + fService.createSnapshot("testStore", null, null); + + fService.createDirectory("testStore:/root", "test"); + + for (int i = 0; i < 10; i++) + { + fService.createFile("testStore:/root/test", ("testFileN" + i + ".txt")).close(); + } + + List diffs = fSyncService.compare(-1, "testStore:/root/", -1, "submitStore:/root/", null); + if (DiffActionEnum.CREATION != action) + { + fSyncService.update(diffs, null, true, true, false, true, null, null); + fSyncService.flatten("testStore:/root/", "submitStore:/root/"); + diffs = fSyncService.compare(-1, "testStore:/root/", -1, "submitStore:/root/", null); + } + + List actual = fSyncService.compare(-1, "testStore:/root/", -1, "submitStore:/root/", null, true); + + if (DiffActionEnum.CREATION == action) + { + assertEquals(11, actual.size()); + + assertEquals(1, diffs.size()); + List newDiff = fSyncService.compare(-1, "testStore:/root/", -1, "submitStore:/root/", null, false); + assertEquals(diffs.toString(), newDiff.toString()); + } + else + { + assertEquals(0, actual.size()); + assertEquals(0, diffs.size()); + } + + String parentPath = "testStore:/root/test/"; + if (DiffActionEnum.CREATION == action) + { + fService.createDirectory(parentPath, "inner"); + parentPath += "inner/"; + } + + int start = (DiffActionEnum.DELETION == action) ? (1) : (0); + int incrementingStep = start + 1; + + for (int i = start; i < 10; i += incrementingStep) + { + String name = "testFileN" + i + ".txt"; + String path = parentPath + name; + + switch (action) + { + case CREATION: + { + fService.createFile(parentPath, name).close(); + break; + } + case MODIFICATION: + { + fService.setNodeProperty(path, WCMModel.PROP_REVERTED_ID, new PropertyValue(WCMModel.PROP_REVERTED_ID, null)); + break; + } + default: + { + fService.removeNode(path); + } + } + } + + int actualModificationsCount = (DiffActionEnum.DELETION == action) ? (5) : ((DiffActionEnum.CREATION == action) ? (22) : (10)); + int diffModificationsCount = (DiffActionEnum.DELETION == action) ? (5) : ((DiffActionEnum.CREATION == action) ? (1) : (10)); + + actual = fSyncService.compare(-1, "testStore:/root", -1, "submitStore:/root", null, true); + diffs = fSyncService.compare(-1, "testStore:/root", -1, "submitStore:/root", null); + + assertEquals(actualModificationsCount, actual.size()); + assertEquals(diffModificationsCount, diffs.size()); + + if (DiffActionEnum.CREATION != action) + { + assertDiffsList(AVMDifference.NEWER, diffs); + assertEquals(diffs.toString(), actual.toString()); + } + + if (DiffActionEnum.CREATION != action) + { + if (DiffActionEnum.DELETION == action) + { + fService.removeNode("testStore:/root/test"); + } + else + { + fService.setNodeProperty("testStore:/root/test", WCMModel.PROP_REVERTED_ID, new PropertyValue(WCMModel.PROP_REVERTED_ID, null)); + } + + actual = fSyncService.compare(-1, "testStore:/root/", -1, "submitStore:/root/", null, true); + diffs = fSyncService.compare(-1, "testStore:/root/", -1, "submitStore:/root/", null); + + actualModificationsCount = (DiffActionEnum.DELETION == action) ? (1) : (actualModificationsCount + 1); + + assertEquals(actualModificationsCount, actual.size()); + assertEquals(1, diffs.size()); + + assertDiffsList(AVMDifference.NEWER, actual); + assertDiffsList(AVMDifference.NEWER, diffs); + } + } + + private void assertDiffsList(int expectedCode, List actual) + { + for (AVMDifference diff : actual) + { + assertNotNull(diff); + assertTrue(diff.isValid()); + assertEquals(expectedCode, diff.getDifferenceCode()); + } + } + public void test_ETWOTWO_570() throws Exception { // Check that read-write methods are properly intercepted diff --git a/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java b/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java index 2417a7bf48..1e9c0e7452 100644 --- a/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMSyncServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -110,9 +110,15 @@ public class AVMSyncServiceImpl implements AVMSyncService public List compare(int srcVersion, String srcPath, int dstVersion, String dstPath, NameMatcher excluder) + { + return compare(srcVersion, srcPath, dstVersion, dstPath, excluder, false); + } + + @Override + public List compare(int srcVersion, String srcPath, int dstVersion, String dstPath, NameMatcher excluder, boolean expandDirs) { long start = System.currentTimeMillis(); - + if (logger.isDebugEnabled()) { logger.debug(srcPath + " : " + dstPath); @@ -131,21 +137,20 @@ public class AVMSyncServiceImpl implements AVMSyncService if (dstDesc == null) { // Special case: no pre-existing version in the destination. - result.add(new AVMDifference(srcVersion, srcPath, - dstVersion, dstPath, - AVMDifference.NEWER)); + result.add(new AVMDifference(srcVersion, srcPath, dstVersion, dstPath, AVMDifference.NEWER)); } else { // Invoke the recursive implementation. - compare(srcVersion, srcDesc, dstVersion, dstDesc, result, excluder, true); + compare(srcVersion, srcDesc, dstVersion, dstDesc, result, excluder, true, expandDirs); } - + if (logger.isDebugEnabled()) { - logger.debug("Raw compare: ["+srcVersion+","+srcPath+"]["+dstVersion+","+dstPath+"]["+result.size()+"] in "+(System.currentTimeMillis()-start)+" msecs"); + logger.debug("Raw compare: [" + srcVersion + "," + srcPath + "][" + dstVersion + "," + dstPath + "][" + result.size() + "] in " + (System.currentTimeMillis() - start) + + " msecs"); } - + return result; } @@ -158,7 +163,7 @@ public class AVMSyncServiceImpl implements AVMSyncService */ private void compare(int srcVersion, AVMNodeDescriptor srcDesc, int dstVersion, AVMNodeDescriptor dstDesc, - List result, NameMatcher excluder, boolean firstLevel) + List result, NameMatcher excluder, boolean firstLevel, boolean expandDirs) { String srcPath = srcDesc.getPath(); String dstPath = dstDesc.getPath(); @@ -225,6 +230,13 @@ public class AVMSyncServiceImpl implements AVMSyncService result.add(new AVMDifference(srcVersion, srcPath, dstVersion, dstPath, dirDiffCode)); + + // Also add all child items if necessary and any exists + if (expandDirs) + { + addNewChildrenIfAny(srcVersion, srcDesc, dstVersion, AVMNodeConverter.ExtendAVMPath(dstPath, dstDesc.getName()), result); + } + return; // short circuit } case AVMDifference.SAME : @@ -269,12 +281,19 @@ public class AVMSyncServiceImpl implements AVMSyncService result.add(new AVMDifference(srcVersion, srcChildPath, dstVersion, dstChildPath, AVMDifference.NEWER)); + + // Also add all child items if necessary and any exists + if (expandDirs) + { + addNewChildrenIfAny(srcVersion, srcChild, dstVersion, dstChildPath, result); + } + continue; } // Otherwise recursively invoke. compare(srcVersion, srcChild, dstVersion, dstChild, - result, excluder, false); + result, excluder, false, expandDirs); } return; } @@ -344,7 +363,7 @@ public class AVMSyncServiceImpl implements AVMSyncService // Otherwise, recursively invoke. compare(srcVersion, srcChild, dstVersion, dstChild, - result, excluder, false); + result, excluder, false, expandDirs); } return; } @@ -378,7 +397,7 @@ public class AVMSyncServiceImpl implements AVMSyncService // Otherwise recursive invocation. compare(srcVersion, srcChild, dstVersion, dstChild, - result, excluder, false); + result, excluder, false, expandDirs); } // Iterate over the destination. for (String name : dstList.keySet()) @@ -412,6 +431,32 @@ public class AVMSyncServiceImpl implements AVMSyncService } } + private void addNewChildrenIfAny(int srcVersion, AVMNodeDescriptor srcChild, int dstVersion, String dstChildPath, List result) + { + Map srcList = fAVMService.getDirectoryListingDirect(srcChild, true); + + for (String name : srcList.keySet()) + { + srcChild = srcList.get(name); + String srcChildPath = srcChild.getPath(); + + String dstPath = AVMNodeConverter.ExtendAVMPath(dstChildPath, name); + AVMNodeDescriptor dstDesc = fAVMService.lookup(dstVersion, dstChildPath, true); + + int diffCode = AVMDifference.NEWER; + if (null == dstDesc) + { + diffCode = AVMDifference.NEWER; + } + result.add(new AVMDifference(srcVersion, srcChildPath, dstVersion, dstPath, diffCode)); + + if (srcChild.isDirectory()) + { + addNewChildrenIfAny(srcVersion, srcChild, dstVersion, dstPath, result); + } + } + } + /** * Updates the destination nodes in the AVMDifferences * with the source nodes. Normally any conflicts or cases in diff --git a/source/java/org/alfresco/repo/avm/AVMSyncServiceTransportImpl.java b/source/java/org/alfresco/repo/avm/AVMSyncServiceTransportImpl.java index 97fac7e97f..a05e8c5fbd 100644 --- a/source/java/org/alfresco/repo/avm/AVMSyncServiceTransportImpl.java +++ b/source/java/org/alfresco/repo/avm/AVMSyncServiceTransportImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -69,6 +69,13 @@ public class AVMSyncServiceTransportImpl implements AVMSyncServiceTransport return fSyncService.compare(srcVersion, srcPath, dstVersion, dstPath, excluder); } + @Override + public List compare(String ticket, int srcVersion, String srcPath, int dstVersion, String dstPath, NameMatcher excluder, boolean expandDirs) + { + fAuthenticationService.validate(ticket); + return fSyncService.compare(srcVersion, srcPath, dstVersion, dstPath, excluder, expandDirs); + } + /* (non-Javadoc) * @see org.alfresco.service.cmr.avmsync.AVMSyncServiceTransport#flatten(java.lang.String, java.lang.String, java.lang.String) */ diff --git a/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java b/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java index fc776385e7..1437a2e0a9 100644 --- a/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java +++ b/source/java/org/alfresco/repo/domain/permissions/ADMAccessControlListDAO.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -36,6 +36,7 @@ import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.util.Pair; +import org.springframework.dao.ConcurrencyFailureException; /** * DAO layer for the improved ACL implementation. This layer is responsible for setting ACLs and any cascade behaviour @@ -369,7 +370,7 @@ public class ADMAccessControlListDAO implements AccessControlListDAO } else if (dbAcl.getAclType() == ACLType.SHARED) { - throw new IllegalStateException(); + throw new ConcurrencyFailureException("setFixedAcls: unexpected shared acl: "+dbAcl); } } } diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index bf00960a82..f15c49d75d 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -1,1268 +1,1273 @@ -/* - * 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; +/* + * 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.AssociationRef; -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.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.security.PersonService; -import org.alfresco.service.cmr.site.SiteInfo; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.util.EqualsHelper; -import org.alfresco.util.FileFilterMode; -import org.alfresco.util.FileFilterMode.Client; -import org.alfresco.util.GUID; -import org.alfresco.util.MaxSizeMap; -import org.alfresco.util.Pair; -import org.alfresco.util.PropertyCheck; -import org.alfresco.util.config.RepositoryFolderConfigBean; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.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); +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); PersonService personService = serviceRegistry.getPersonService(); NodeRef userRef = personService.getPerson(user.getLogin()); nodeService.removeAssociation(userRef, mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASSOC_IMAP_UNSUBSCRIBED); - } - - public void unsubscribe(AlfrescoImapUser user, String mailbox) - { - if (logger.isDebugEnabled()) - { - logger.debug("Unsubscribing: " + user + ", " + mailbox); - } - AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false); - if(mailFolder.getFolderInfo() != null) - { + } + + public void unsubscribe(AlfrescoImapUser user, String mailbox) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unsubscribing: " + user + ", " + mailbox); + } + AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false); + if(mailFolder.getFolderInfo() != null) + { PersonService personService = serviceRegistry.getPersonService(); NodeRef userRef = personService.getPerson(user.getLogin()); nodeService.createAssociation(userRef, mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASSOC_IMAP_UNSUBSCRIBED); - } - else - { - // perhaps the folder has been deleted by another async process? - logger.debug("Unable to find folder to unsubscribe"); - } - } - - /** - * Return flags that belong to the specified imap folder. - * - * @param messageInfo imap folder info. - * @return flags. - */ - public Flags getFlags(FileInfo messageInfo) - { - Flags flags = new Flags(); - Map 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); - - String permission = (flag == Flag.DELETED ? PermissionService.DELETE_NODE : PermissionService.WRITE_PROPERTIES); - - AccessStatus status = permissionService.hasPermission(nodeRef, permission); - if (status == AccessStatus.DENIED) - { - if(flag == Flag.DELETED) - { - logger.debug("[setFlag] Access denied to set DELETED FLAG:" + nodeRef); - throw new AccessDeniedException("No permission to set DELETED flag"); - } - if(flag == Flag.SEEN) - { - logger.debug("[setFlag] Access denied to set SEEN FLAG:" + nodeRef); - //TODO - should we throw an exception here? - //throw new AccessDeniedException("No permission to set DELETED flag"); - } - else - { - logger.debug("[setFlag] Access denied to set flag:" + nodeRef); - throw new AccessDeniedException("No permission to set flag:" + flag.toString()); - } - } - else - { - 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; - + } + 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 message. + * + * @param messageInfo FileInfo of imap Folder + * @param flag flag to set. + * @param value value value to set. + */ + public void setFlag(FileInfo messageInfo, Flag flag, boolean value) + { + setFlag(messageInfo.getNodeRef(), flag, value); + } + + private void setFlag(NodeRef nodeRef, Flag flag, boolean value) + { + String permission = (flag == Flag.DELETED ? PermissionService.DELETE_NODE : PermissionService.WRITE_PROPERTIES); + + + AccessStatus status = permissionService.hasPermission(nodeRef, permission); + if (status == AccessStatus.DENIED) + { + if(flag == Flag.DELETED) + { + logger.debug("[setFlag] Access denied to set DELETED FLAG:" + nodeRef); + throw new AccessDeniedException("No permission to set DELETED flag"); + } + if(flag == Flag.SEEN) + { + logger.debug("[setFlag] Access denied to set SEEN FLAG:" + nodeRef); + //TODO - should we throw an exception here? + //throw new AccessDeniedException("No permission to set DELETED flag"); + } + else + { + + logger.debug("[setFlag] Access denied to set flag:" + nodeRef); + throw new AccessDeniedException("No permission to set flag:" + flag.toString()); + } + } + else + { + policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + + checkForFlaggableAspect(nodeRef); + + 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; + String userName = user.getLogin(); Set unsubscribedFodlers = getUnsubscribedFolders(userName); - for (String mountPointName : imapConfigMountPoints.keySet()) - { - if (mountPointName.matches(rootPath.replaceAll("[%\\*]", ".*"))) - { - NodeRef mountPoint = getMountPoint(mountPointName); - if (mountPoint != null) - { - int mountPointId = mountPointIds.get(mountPointName); - FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint); - ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode(); - if (index < 0) - { + for (String mountPointName : imapConfigMountPoints.keySet()) + { + if (mountPointName.matches(rootPath.replaceAll("[%\\*]", ".*"))) + { + NodeRef mountPoint = getMountPoint(mountPointName); + if (mountPoint != null) + { + int mountPointId = mountPointIds.get(mountPointName); + FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint); + ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode(); + if (index < 0) + { if (!listSubscribed || !unsubscribedFodlers.contains(mountPointFileInfo.getNodeRef())) - { - result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, - isExtractionEnabled(mountPointFileInfo.getNodeRef()), 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) - { + { + 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) + { String userName = user.getLogin(); Set unsubscribedFodlers = getUnsubscribedFolders(userName); - // This is the last level - for (FileInfo fileInfo : list) - { - if (!filter.isEnterSubfolder(fileInfo.getNodeRef())) - { - continue; - } - String folderPath = rootPathPrefix + fileInfo.getName(); + // This is the last level + for (FileInfo fileInfo : list) + { + if (!filter.isEnterSubfolder(fileInfo.getNodeRef())) + { + continue; + } + String folderPath = rootPathPrefix + fileInfo.getName(); if (!listSubscribed || !unsubscribedFodlers.contains(fileInfo.getNodeRef())) - { - fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode, - isExtractionEnabled(fileInfo.getNodeRef()), 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; - } - + { + 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 Set getUnsubscribedFolders(String userName) - { + { Set result = new HashSet(); PersonService personService = serviceRegistry.getPersonService(); NodeRef userRef = personService.getPerson(userName); @@ -1273,840 +1278,840 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } return result; - } - + } - private String getCurrentUser() - { - return AuthenticationUtil.getFullyAuthenticatedUser(); - } - - /** - * Return list of "favourite" sites, that belong to the specified user and are marked as "Imap favourite" - * - * @param userName name of user - * @return List of favourite sites. - */ - private List 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; - } - } + + 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/management/DynamicMBeanExportOperations.java b/source/java/org/alfresco/repo/management/DynamicMBeanExportOperations.java new file mode 100644 index 0000000000..aa144c1f01 --- /dev/null +++ b/source/java/org/alfresco/repo/management/DynamicMBeanExportOperations.java @@ -0,0 +1,38 @@ +/* + * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.repo.management; + +import javax.management.ObjectName; + +/** + * An interface that allows individual MBeans to be registered and unregistered over time. + * + * @author dward + */ +public interface DynamicMBeanExportOperations +{ + /** + * Unregisters an MBean + * + * @param objectName + * the object name + */ + public void unregisterMBean(ObjectName objectName); + + /** + * Registers an MBean. + * + * @param managedResource + * the managed resource + * @param objectName + * the object name + * @return the actual object name + */ + public ObjectName registerMBean(Object managedResource, ObjectName objectName); + +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/management/DynamicMBeanExporter.java b/source/java/org/alfresco/repo/management/DynamicMBeanExporter.java new file mode 100644 index 0000000000..4ce35b6993 --- /dev/null +++ b/source/java/org/alfresco/repo/management/DynamicMBeanExporter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.repo.management; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.support.MBeanRegistrationSupport; + +/** + * An {@link MBeanExporter} that allows individual MBeans to be registered and unregistered over time. + */ +public class DynamicMBeanExporter extends MBeanExporter implements DynamicMBeanExportOperations +{ + static private ThreadLocal threadServer = new ThreadLocal(); + + /** + * Instantiates a new dynamic MBean exporter. + */ + public DynamicMBeanExporter() + { + // For consistency, try to continue to use the last MBeanServer used in the same thread + MBeanServer server = threadServer.get(); + if (server != null) + { + setServer(server); + } + + // Make replace existing the default registration behavior + setRegistrationBehavior(MBeanRegistrationSupport.REGISTRATION_REPLACE_EXISTING); + setAutodetectMode(MBeanExporter.AUTODETECT_NONE); + } + + @Override + public void setServer(MBeanServer server) + { + threadServer.set(server); + super.setServer(server); + } + + /* + * (non-Javadoc) + * @see + * org.alfresco.enterprise.repo.management.DynamicMBeanExportOperations#unregisterMBean(javax.management.ObjectName) + */ + public void unregisterMBean(ObjectName objectName) + { + if (this.registeredBeans.remove(objectName)) + { + try + { + this.server.unregisterMBean(objectName); + onUnregister(objectName); + } + catch (JMException e) + { + throw new RuntimeException(e); + } + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.enterprise.repo.management.DynamicMBeanExportOperations#registerMBean(java.lang.Object, + * javax.management.ObjectName) + */ + @SuppressWarnings("unchecked") + public ObjectName registerMBean(Object managedResource, ObjectName objectName) + { + Object mbean; + if (isMBean(managedResource.getClass())) + { + mbean = managedResource; + } + else + { + mbean = createAndConfigureMBean(managedResource, managedResource.getClass().getName()); + } + ObjectName actualObjectName = objectName; + try + { + try + { + actualObjectName = this.server.registerMBean(mbean, objectName).getObjectName(); + } + catch (InstanceAlreadyExistsException ex) + { + this.server.unregisterMBean(objectName); + actualObjectName = this.server.registerMBean(mbean, objectName).getObjectName(); + } + } + catch (JMException e) + { + throw new RuntimeException(e); + } + this.registeredBeans.add(actualObjectName); + onRegister(actualObjectName); + return actualObjectName; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/remote/AVMSyncServiceRemote.java b/source/java/org/alfresco/repo/remote/AVMSyncServiceRemote.java index edd6412641..1554aad7f7 100644 --- a/source/java/org/alfresco/repo/remote/AVMSyncServiceRemote.java +++ b/source/java/org/alfresco/repo/remote/AVMSyncServiceRemote.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -100,4 +100,10 @@ public class AVMSyncServiceRemote implements AVMSyncService { fTransport.update(fTicketHolder.getTicket(), diffList, excluder, ignoreConflicts, ignoreOlder, overrideConflicts, overrideOlder, tag, description); } + + @Override + public List compare(int srcVersion, String srcPath, int dstVersion, String dstPath, NameMatcher excluder, boolean expandDirs) + { + return fTransport.compare(fTicketHolder.getTicket(), srcVersion, srcPath, dstVersion, dstPath, excluder, expandDirs); + } } diff --git a/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java b/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java index 583e174c4e..7a18ed22e3 100644 --- a/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java +++ b/source/java/org/alfresco/repo/security/authentication/AbstractAuthenticationComponent.java @@ -259,7 +259,7 @@ public abstract class AbstractAuthenticationComponent implements AuthenticationC logger.debug("Setting the current user to the guest user of tenant domain \"" + tenantDomain + '"'); } GrantedAuthority[] gas = new GrantedAuthority[0]; - ud = new User(getGuestUserName(tenantDomain), "", true, true, true, true, gas); + ud = new User(userName, "", true, true, true, true, gas); } else { diff --git a/source/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java b/source/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java index 22e164c237..b0269ba2cf 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -1223,7 +1223,7 @@ public class WorkflowServiceImpl implements WorkflowService final String packageAvmPath = AVMNodeConverter.ToAVMVersionPath(workflowPackage).getSecond(); if (logger.isDebugEnabled()) logger.debug("comparing " + packageAvmPath + " with " + stagingAvmPath); - for (AVMDifference d : avmSyncService.compare(-1, packageAvmPath, -1, stagingAvmPath, null)) + for (AVMDifference d : avmSyncService.compare(-1, packageAvmPath, -1, stagingAvmPath, null, true)) { if (logger.isDebugEnabled()) logger.debug("got difference " + d); diff --git a/source/java/org/alfresco/service/cmr/avmsync/AVMSyncService.java b/source/java/org/alfresco/service/cmr/avmsync/AVMSyncService.java index 8764604aba..66c6c31f1c 100644 --- a/source/java/org/alfresco/service/cmr/avmsync/AVMSyncService.java +++ b/source/java/org/alfresco/service/cmr/avmsync/AVMSyncService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -42,7 +42,17 @@ public interface AVMSyncService public List compare(int srcVersion, String srcPath, int dstVersion, String dstPath, NameMatcher excluder); - + + /** + * Get a difference list between two corresponding node trees + * + * Note: new/modified directories can be optionally expanded to include new/modified children + */ + public List compare(int srcVersion, String srcPath, + int dstVersion, String dstPath, + NameMatcher excluder, + boolean expandDirs); + /** * Updates the destination nodes in the AVMDifferences * with the source nodes. Normally any conflicts or cases in diff --git a/source/java/org/alfresco/service/cmr/remote/AVMSyncServiceTransport.java b/source/java/org/alfresco/service/cmr/remote/AVMSyncServiceTransport.java index 008375157e..c0526e8a91 100644 --- a/source/java/org/alfresco/service/cmr/remote/AVMSyncServiceTransport.java +++ b/source/java/org/alfresco/service/cmr/remote/AVMSyncServiceTransport.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -42,7 +42,23 @@ public interface AVMSyncServiceTransport int srcVersion, String srcPath, int dstVersion, String dstPath, NameMatcher excluder); - + + /** + * Get a difference list between two corresponding node trees. New/modified children in new/modified directories will be also included + * + * @param srcVersion The version id for the source tree. + * @param srcPath The avm path to the source tree. + * @param dstVersion The version id for the destination tree. + * @param dstPath The avm path to the destination tree. + * @param expandDirs {@link Boolean} value that determines whether new/modified children in new/modified directories be included into result + * @return A List of AVMDifference structs which can be used for + * the update operation. + */ + public List compare(String ticket, + int srcVersion, String srcPath, + int dstVersion, String dstPath, + NameMatcher excluder, boolean expandDirs); + /** * Updates the destination nodes in the AVMDifferences * with the source nodes. Normally any conflicts or cases in diff --git a/source/java/org/alfresco/wcm/util/WCMWorkflowUtil.java b/source/java/org/alfresco/wcm/util/WCMWorkflowUtil.java index 76777bf0ac..77d04a2bdd 100644 --- a/source/java/org/alfresco/wcm/util/WCMWorkflowUtil.java +++ b/source/java/org/alfresco/wcm/util/WCMWorkflowUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -180,7 +180,7 @@ public class WCMWorkflowUtil String wfPath = AVMNodeConverter.ToAVMVersionPath(ref).getSecond(); String stagingSandboxPath = WCMUtil.getCorrespondingPath(wfPath, stagingSandboxName); - List diffs = avmSyncService.compare(-1, wfPath, -1, stagingSandboxPath, null); + List diffs = avmSyncService.compare(-1, wfPath, -1, stagingSandboxPath, null, true); for (AVMDifference diff : diffs) {