From 3d61b9af7a090114c2e581143ce3dfd772190a5f Mon Sep 17 00:00:00 2001 From: Alan Davis Date: Thu, 18 Sep 2014 17:20:55 +0000 Subject: [PATCH] Merged HEAD-BUG-FIX (5.0/Cloud) to HEAD (5.0/Cloud) 84028: Merged V4.2-BUG-FIX (4.2.4) to HEAD-BUG-FIX (5.0/Cloud) 83341: Merged DEV to V4.2-BUG-FIX (4.2.4) 82263: MNT-12259 : Outlook 2013 implements an IMAP move as a \Deleted + APPEND leading to corrupt files Implemented a complex move operation to support Outlook 2013. 82465: MNT-12259 : Outlook 2013 implements an IMAP move as a \Deleted + APPEND leading to misleading copy of alfresco dummy file Modified the fix to use the message id to determine the complex move operation. Added JUnit test. 83420: MNT-12259 : Outlook 2013 implements an IMAP move as a \Deleted + APPEND leading to misleading copy of alfresco dummy file Added an addition check for node existence to correctly handle invalid nodeRefs in message id. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@84619 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../repo/imap/AlfrescoImapFolder.java | 112 +++++++++++++++++- .../alfresco/repo/imap/ImapServiceImpl.java | 66 ++++++++++- .../repo/imap/ImapServiceImplTest.java | 56 ++++++++- 3 files changed, 229 insertions(+), 5 deletions(-) diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java index 1aa8f6b162..50b3fdcf5e 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2014 Alfresco Software Limited. * * This file is part of Alfresco * @@ -43,8 +43,12 @@ import org.alfresco.service.cmr.model.FileExistsException; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; 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.security.AccessStatus; +import org.alfresco.util.FileFilterMode; import org.alfresco.util.GUID; import org.alfresco.util.Utf7; import org.apache.commons.logging.Log; @@ -309,12 +313,116 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab Date internalDate) throws FileExistsException, FileNotFoundException, IOException, MessagingException { - long uid = createMimeMessageInFolder(this.folderInfo, message, flags); + long uid; + NodeRef sourceNodeRef = extractNodeRef(message); + if (isMoveOperation(sourceNodeRef)) + { + uid = moveNode(this.folderInfo, message, flags, sourceNodeRef); + } + else + { + uid = createMimeMessageInFolder(this.folderInfo, message, flags); + } // Invalidate current folder status this.folderStatus = null; return uid; } + /** + * Moves the node sourceNodeRef extracted from the message id. + * A part of a complex move operation. + * + * @param folderInfo + * @param message + * @param flags + * @param sourceNodeRef + * @return UUID of the moved node + * @throws FileExistsException + * @throws FileNotFoundException + */ + @SuppressWarnings("deprecation") + private long moveNode(FileInfo folderInfo, MimeMessage message, Flags flags, NodeRef sourceNodeRef) + throws FileExistsException, FileNotFoundException + { + FileFolderService fileFolderService = serviceRegistry.getFileFolderService(); + FileFilterMode.setClient(FileFilterMode.Client.imap); + fileFolderService.setHidden(sourceNodeRef, false); + FileInfo messageFile = fileFolderService.move(sourceNodeRef, folderInfo.getNodeRef(), null); + final long newMessageUid = (Long) messageFile.getProperties().get(ContentModel.PROP_NODE_DBID); + imapService.setFlag(messageFile, Flag.RECENT, true); + imapService.setFlag(messageFile, Flag.DELETED, false); + return newMessageUid; + } + + /** + * Extract a NodeRef from the message id. + *
Typical message id is "<74bad8aa-75a5-4063-8e46-9d1b5737f43b@alfresco.org>" + *
See {@link AbstractMimeMessage#updateMessageID()} + * + * @param message + * @return null if nothing is found + */ + private NodeRef extractNodeRef(MimeMessage message) + { + String uuid = null; + String messageId = null; + NodeRef result = null; + try + { + messageId = message.getMessageID(); + } + catch (MessagingException me) + { + // we cannot use message id to extract nodeRef + } + + if (messageId != null) + { + if (messageId.startsWith("<")) + { + messageId = messageId.substring(1); + } + if (messageId.indexOf("@") != -1) + { + uuid = messageId.substring(0, messageId.indexOf("@")); + } + else + { + uuid = messageId; + } + result = new NodeRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore", uuid); + } + return result; + } + + /** + * Determine if it is a complex move operation, which consists of a create superseded by a delete. + * + * @param sourceNodeRef + * @return + */ + @SuppressWarnings("deprecation") + private boolean isMoveOperation(NodeRef sourceNodeRef) + { + if (sourceNodeRef != null) + { + NodeService nodeService = serviceRegistry.getNodeService(); + if (nodeService.exists(sourceNodeRef)) + { + FileFolderService fileFolderService = serviceRegistry.getFileFolderService(); + FileInfo node = fileFolderService.getFileInfo(sourceNodeRef); + if (node != null) + { + if (fileFolderService.isHidden(sourceNodeRef)) + { + return true; + } + } + } + } + return false; + } + /** * Copies message with the given UID to the specified {@link MailFolder}. * diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index 6fab3fe7a8..92acffa143 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -34,6 +34,8 @@ import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; import java.util.TreeMap; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -158,6 +160,8 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea private final static Map qNameToFlag; private final static Map flagToQname; + private static final Timer deleteDelayTimer = new Timer(); + private boolean imapServerEnabled = false; static @@ -538,11 +542,71 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea Flags flags = getFlags(fileInfo); if (flags.contains(Flags.Flag.DELETED)) { - fileFolderService.delete(fileInfo.getNodeRef()); + // See MNT-12259 + //fileFolderService.delete(fileInfo.getNodeRef()); + hideAndDelete(fileInfo.getNodeRef()); messageCache.remove(fileInfo.getNodeRef()); } } + /** + * Workaround for MNT-12259 + * @param nodeRef + */ + @SuppressWarnings("deprecation") + private void hideAndDelete(final NodeRef nodeRef) + { + FileFilterMode.setClient(FileFilterMode.Client.imap); + fileFolderService.setHidden(nodeRef, true); + { + // Get the current user + final String deleteDelayUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // Add a timed task to really delete the file + TimerTask deleteDelayTask = new TimerTask() + { + @Override + public void run() + { + RunAsWork deleteDelayRunAs = new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + // Ignore if it is NOT hidden: the shuffle may have finished; the operation may have failed + if (!nodeService.exists(nodeRef) || !fileFolderService.isHidden(nodeRef)) + { + return null; + } + + // Since this will run in a different thread, the client thread-local must be set + // or else unhiding the node will not unhide it for IMAP. + FileFilterMode.setClient(FileFilterMode.Client.imap); + + // Unhide the node, e.g. for archiving + fileFolderService.setHidden(nodeRef, false); + + // This is the transaction-aware service + fileFolderService.delete(nodeRef); + return null; + } + }; + try + { + AuthenticationUtil.runAs(deleteDelayRunAs, deleteDelayUser); + } + catch (Throwable e) + { + // consume exception to avoid it leaking from the TimerTask and causing the Timer to + // no longer accept tasks to be scheduled. + logger.info("Exception thrown during IMAP delete timer task.", e); + } + } + }; + // Schedule a real delete 5 seconds after the current time + deleteDelayTimer.schedule(deleteDelayTask, 5000L); + } + } + public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist, boolean mayCreate) { if (mailboxName == null) diff --git a/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java b/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java index c1aa4b0402..b4a3387b37 100644 --- a/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java +++ b/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2013 Alfresco Software Limited. + * Copyright (C) 2005-2014 Alfresco Software Limited. * * This file is part of Alfresco * @@ -54,6 +54,7 @@ import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -69,12 +70,15 @@ import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; import org.alfresco.util.PropertyMap; import org.alfresco.util.config.RepositoryFolderConfigBean; import org.junit.experimental.categories.Category; import org.springframework.context.ApplicationContext; import org.springframework.core.io.ClassPathResource; +import com.icegreen.greenmail.store.SimpleStoredMessage; + /** * Unit test for ImapServiceImpl */ @@ -107,6 +111,7 @@ public class ImapServiceImplTest extends TestCase private AlfrescoImapUser user; private ImapService imapService; private UserTransaction txn; + private ContentService contentService; private NodeRef testImapFolderNodeRef; private Flags flags; @@ -128,7 +133,7 @@ public class ImapServiceImplTest extends TestCase searchService = serviceRegistry.getSearchService(); namespaceService = serviceRegistry.getNamespaceService(); fileFolderService = serviceRegistry.getFileFolderService(); - + contentService = serviceRegistry.getContentService(); flags = new Flags(); flags.add(Flags.Flag.SEEN); @@ -869,6 +874,53 @@ public class ImapServiceImplTest extends TestCase assertPathHierarchy(fullPathList, pathAfterRenaming); } + /** + * Test for MNT-12259 + * There is a 5s gap to run the test, see {@link ImapServiceImpl#hideAndDelete} + * + * @throws Exception + */ + public void testMoveViaDeleteAndAppend() throws Exception + { + AlfrescoImapUser poweredUser = new AlfrescoImapUser((USER_NAME + "@alfresco.com"), USER_NAME, USER_PASSWORD); + String fileName = "testfile" + GUID.generate(); + String destinationName = "testFolder" + GUID.generate(); + String destinationPath = IMAP_ROOT + AlfrescoImapConst.HIERARCHY_DELIMITER + destinationName; + String nodeContent = "test content"; + NodeRef root = findCompanyHomeNodeRef(); + AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser(); + + // Create node and destination folder + FileInfo origFile = fileFolderService.create(root, fileName, ContentModel.TYPE_CONTENT); + ContentWriter contentWriter = contentService.getWriter(origFile.getNodeRef(), ContentModel.PROP_CONTENT, true); + contentWriter.setMimetype("text/plain"); + contentWriter.setEncoding("UTF-8"); + contentWriter.putContent(nodeContent); + + FileInfo destinationNode = fileFolderService.create(root, destinationName, ContentModel.TYPE_FOLDER); + nodeService.addAspect(origFile.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT, null); + + // Save the message and ensure the message id is set + SimpleStoredMessage origMessage = imapService.getMessage(origFile); + origMessage.getMimeMessage().saveChanges(); + + imapService.setFlag(origFile, Flags.Flag.DELETED, true); + // Delete the node + imapService.expungeMessage(origFile); + + // Append the message to destination + AlfrescoImapFolder destinationMailbox = imapService.getOrCreateMailbox(poweredUser, destinationPath, true, false); + destinationMailbox.appendMessage(origMessage.getMimeMessage(), flags, null); + + // Check the destination has the original file and only this file + FileInfo movedNode = fileFolderService.getFileInfo(origFile.getNodeRef()); + assertNotNull("The file should exist.", movedNode); + assertEquals("The file name should not change.", fileName, movedNode.getName()); + NodeRef newParentNodeRef = nodeService.getPrimaryParent(origFile.getNodeRef()).getParentRef(); + assertEquals("The parent should change to destination.", destinationNode.getNodeRef(), newParentNodeRef); + assertEquals("There should be only one node in the destination folder", 1, nodeService.getChildAssocs(destinationNode.getNodeRef()).size()); + } + /** * @param mailbox - {@link AlfrescoImapFolder} instance which should be checked */