diff --git a/config/alfresco/model/imapModel.xml b/config/alfresco/model/imapModel.xml index 1d79886e02..f31df99dbe 100644 --- a/config/alfresco/model/imapModel.xml +++ b/config/alfresco/model/imapModel.xml @@ -197,6 +197,16 @@ + + IMAP Message Headers + + + Message Headers + d:text + true + + + \ No newline at end of file diff --git a/config/alfresco/subsystems/imap/default/imap-server-context.xml b/config/alfresco/subsystems/imap/default/imap-server-context.xml index 5222a989ed..4462b36e15 100644 --- a/config/alfresco/subsystems/imap/default/imap-server-context.xml +++ b/config/alfresco/subsystems/imap/default/imap-server-context.xml @@ -184,6 +184,11 @@ ${imap.server.enabled} + + + Message-Id + + diff --git a/pom.xml b/pom.xml index 3e08a4afca..70e8f4e097 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ com.icegreen greenmail - 1.3-alfresco-20130711 + 1.3-alfresco-20141112 commons-dbcp diff --git a/source/java/org/alfresco/model/ImapModel.java b/source/java/org/alfresco/model/ImapModel.java index cc7afb99ab..e5c24f8765 100644 --- a/source/java/org/alfresco/model/ImapModel.java +++ b/source/java/org/alfresco/model/ImapModel.java @@ -65,5 +65,10 @@ public interface ImapModel static final QName ASPECT_IMAP_PREFERENCES = QName.createQName(IMAP_MODEL_1_0_URI, "imapPreferences"); static final QName ASSOC_IMAP_UNSUBSCRIBED = QName.createQName(IMAP_MODEL_1_0_URI, "imapUnsubscribed"); + + static final QName ASPECT_IMAP_MESSAGE_HEADERS = QName.createQName(IMAP_MODEL_1_0_URI, "messageHeaders"); + static final QName PROP_MESSAGE_HEADERS = QName.createQName(IMAP_MODEL_1_0_URI, "messageHeaders"); + + static final String MESSAGE_HEADER_TO_PERSIST_SPLITTER = ":"; } diff --git a/source/java/org/alfresco/repo/imap/AbstractImapFolder.java b/source/java/org/alfresco/repo/imap/AbstractImapFolder.java index d01be042da..9cf9bf5897 100644 --- a/source/java/org/alfresco/repo/imap/AbstractImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AbstractImapFolder.java @@ -49,7 +49,7 @@ public abstract class AbstractImapFolder implements MailFolder private List listeners = new LinkedList(); protected ServiceRegistry serviceRegistry; - protected static int MAX_RETRIES = 1; + protected static int MAX_RETRIES = 20; public AbstractImapFolder(ServiceRegistry serviceRegistry) @@ -103,7 +103,7 @@ public abstract class AbstractImapFolder implements MailFolder * @param uid - UID of the message * @param toFolder - reference to the destination folder. */ - public void copyMessage(final long uid, final MailFolder toFolder) throws FolderException + public long copyMessage(final long uid, final MailFolder toFolder) throws FolderException { AbstractImapFolder toImapMailFolder = (AbstractImapFolder) toFolder; @@ -112,15 +112,14 @@ public abstract class AbstractImapFolder implements MailFolder throw new FolderException(AlfrescoImapFolderException.PERMISSION_DENIED); } - CommandCallback command = new CommandCallback() + CommandCallback command = new CommandCallback() { - public Object command() throws Throwable + public Long command() throws Throwable { - copyMessageInternal(uid, toFolder); - return null; + return copyMessageInternal(uid, toFolder); } }; - command.runFeedback(); + return command.runFeedback(); } /** @@ -161,6 +160,25 @@ public abstract class AbstractImapFolder implements MailFolder command.runFeedback(); } + /** + * Deletes messages marked with {@link Flags.Flag#DELETED}. Note that this message deletes the messages with current uid + */ + public void expunge(final long uid) throws FolderException + { + if (isReadOnly()) + { + throw new FolderException("Can't expunge - Permission denied"); + } + CommandCallback command = new CommandCallback() + { + public Object command() throws Throwable + { + expungeInternal(uid); + return null; + } + }; + command.runFeedback(); + } /** * Returns message by its UID. @@ -258,14 +276,21 @@ public abstract class AbstractImapFolder implements MailFolder /** - * Simply returns UIDs of all messages in the folder. + * Searches the mailbox for messages that match the given searching criteria * - * @param searchTerm - not used + * @param searchTerm - search term that contains search criteria. * @return UIDs of the messages */ - public long[] search(SearchTerm searchTerm) + public long[] search(final SearchTerm searchTerm) { - return getMessageUids(); + CommandCallback command = new CommandCallback() + { + public long[] command() throws Throwable + { + return searchInternal(searchTerm); + } + }; + return command.run(); } /** @@ -377,12 +402,14 @@ public abstract class AbstractImapFolder implements MailFolder protected abstract long appendMessageInternal(MimeMessage message, Flags flags, Date internalDate) throws Exception; - protected abstract void copyMessageInternal(long uid, MailFolder toFolder) throws Exception; + protected abstract long copyMessageInternal(long uid, MailFolder toFolder) throws Exception; protected abstract void deleteAllMessagesInternal() throws Exception; protected abstract void expungeInternal() throws Exception; + protected abstract void expungeInternal(long uid) throws Exception; + protected abstract SimpleStoredMessage getMessageInternal(long uid) throws Exception; protected abstract List getMessagesInternal(); @@ -393,6 +420,8 @@ public abstract class AbstractImapFolder implements MailFolder protected abstract void replaceFlagsInternal(Flags flags, long uid, FolderListener silentListener, boolean addUid) throws Exception; + protected abstract long[] searchInternal(SearchTerm searchTerm); + protected abstract void setFlagsInternal(Flags flags, boolean value, long uid, FolderListener silentListener, boolean addUid) throws Exception; protected abstract class CommandCallback diff --git a/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java b/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java index a6ff8488d0..e9fdbb1053 100644 --- a/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java +++ b/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java @@ -23,6 +23,7 @@ import static org.alfresco.repo.imap.AlfrescoImapConst.X_ALF_NODEREF_ID; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; @@ -31,6 +32,7 @@ import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.internet.MimeMessage; +import org.alfresco.model.ImapModel; import org.alfresco.repo.imap.ImapService.EmailBodyFormat; import org.alfresco.repo.template.TemplateNode; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -38,6 +40,7 @@ import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransacti import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -119,6 +122,31 @@ public abstract class AbstractMimeMessage extends MimeMessage // Optional headers for further implementation of multiple Alfresco server support. setHeader(X_ALF_NODEREF_ID, messageFileInfo.getNodeRef().getId()); // setHeader(X_ALF_SERVER_UID, imapService.getAlfrescoServerUID()); + + setPersistedHeaders(); + } + + private void setPersistedHeaders() throws MessagingException + { + NodeService nodeService = serviceRegistry.getNodeService(); + if (nodeService.hasAspect(messageFileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_MESSAGE_HEADERS)) + { + @SuppressWarnings("unchecked") + List messageHeaders = (List)nodeService.getProperty(messageFileInfo.getNodeRef(), ImapModel.PROP_MESSAGE_HEADERS); + + if (messageHeaders == null) + { + return; + } + + for (String header : messageHeaders) + { + String headerValue = header.substring(header.indexOf(ImapModel.MESSAGE_HEADER_TO_PERSIST_SPLITTER) + 1); + String headerName = header.substring(0, header.indexOf(ImapModel.MESSAGE_HEADER_TO_PERSIST_SPLITTER)); + + setHeader(headerName, headerValue); + } + } } diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java index 0312754ef1..0da958c09a 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java @@ -21,6 +21,7 @@ package org.alfresco.repo.imap; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -33,21 +34,23 @@ import javax.mail.Flags; import javax.mail.Flags.Flag; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; +import javax.mail.search.SearchTerm; import org.alfresco.model.ContentModel; import org.alfresco.model.ImapModel; import org.alfresco.repo.imap.AlfrescoImapConst.ImapViewMode; import org.alfresco.repo.imap.ImapService.FolderStatus; +import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.service.ServiceRegistry; 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.service.namespace.QName; import org.alfresco.util.FileFilterMode; import org.alfresco.util.GUID; import org.alfresco.util.Utf7; @@ -285,18 +288,14 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab protected FolderStatus getFolderStatus() { - if (this.folderStatus == null) + CommandCallback command = new CommandCallback() { - CommandCallback command = new CommandCallback() + public FolderStatus command() throws Throwable { - public FolderStatus command() throws Throwable - { - return imapService.getFolderStatus(userName, folderInfo.getNodeRef(), viewMode); - } - }; - this.folderStatus = command.run(); - } - return this.folderStatus; + return imapService.getFolderStatus(userName, folderInfo.getNodeRef(), viewMode); + } + }; + return this.folderStatus = command.run(); } /** @@ -315,13 +314,9 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab { long uid; NodeRef sourceNodeRef = extractNodeRef(message); - if (isMoveOperation(sourceNodeRef)) + if (sourceNodeRef != null) { - uid = copyOrmoveNode(this.folderInfo, message, flags, sourceNodeRef, true); - } - else if (sourceNodeRef != null) - { - uid = copyOrmoveNode(this.folderInfo, message, flags, sourceNodeRef, false); + uid = copyOrMoveNode(this.folderInfo, message, flags, sourceNodeRef, false); } else { @@ -345,25 +340,34 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @throws FileNotFoundException */ @SuppressWarnings("deprecation") - private long copyOrmoveNode(FileInfo folderInfo, MimeMessage message, Flags flags, NodeRef sourceNodeRef, boolean move) + private long copyOrMoveNode(FileInfo folderInfo, MimeMessage message, Flags flags, NodeRef sourceNodeRef, boolean move) throws FileExistsException, FileNotFoundException { FileFolderService fileFolderService = serviceRegistry.getFileFolderService(); FileFilterMode.setClient(FileFilterMode.Client.imap); - fileFolderService.setHidden(sourceNodeRef, false); FileInfo messageFile = null; if (move) { + fileFolderService.setHidden(sourceNodeRef, false); messageFile = fileFolderService.move(sourceNodeRef, folderInfo.getNodeRef(), null); } else { - messageFile = fileFolderService.copy(sourceNodeRef, folderInfo.getNodeRef(), null); + NodeRef newNodeRef = serviceRegistry.getCopyService().copyAndRename(sourceNodeRef, folderInfo.getNodeRef(), ContentModel.ASSOC_CONTAINS, null, false); + fileFolderService.setHidden(newNodeRef, false); + messageFile = fileFolderService.getFileInfo(newNodeRef); } final long newMessageUid = (Long) messageFile.getProperties().get(ContentModel.PROP_NODE_DBID); - imapService.setFlag(messageFile, Flag.RECENT, true); + + imapService.persistMessageHeaders(messageFile.getNodeRef(), message); + + Flags newFlags = new Flags(flags); + newFlags.add(Flag.RECENT); + + imapService.setFlags(messageFile, newFlags, true); imapService.setFlag(messageFile, Flag.DELETED, false); - return newMessageUid; + + return newMessageUid; } /** @@ -472,7 +476,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @throws FileExistsException */ @Override - protected void copyMessageInternal( + protected long copyMessageInternal( long uid, MailFolder toFolder) throws MessagingException, FileExistsException, FileNotFoundException, IOException { @@ -486,13 +490,14 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab { //Generate body of message MimeMessage newMessage = new ImapModelMessage(sourceMessageFileInfo, serviceRegistry, true); - toImapMailFolder.appendMessageInternal(newMessage, imapService.getFlags(sourceMessageFileInfo), new Date()); + return toImapMailFolder.appendMessageInternal(newMessage, imapService.getFlags(sourceMessageFileInfo), new Date()); } else { String fileName = (String) serviceRegistry.getNodeService().getProperty(sourceMessageFileInfo.getNodeRef(), ContentModel.PROP_NAME); String newFileName = imapService.generateUniqueFilename(destFolderNodeRef, fileName); - serviceRegistry.getFileFolderService().copy(sourceMessageFileInfo.getNodeRef(), destFolderNodeRef, newFileName); + FileInfo messageFileInfo = serviceRegistry.getFileFolderService().copy(sourceMessageFileInfo.getNodeRef(), destFolderNodeRef, newFileName); + return (Long)messageFileInfo.getProperties().get(ContentModel.PROP_NODE_DBID); } } @@ -531,7 +536,23 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab imapService.expungeMessage(entry.getValue()); } } + + /** + * Deletes messages marked with {@link Flags.Flag#DELETED}. Note that this message deletes the message with current uid + */ + @Override + protected void expungeInternal(long uid) throws Exception + { + if (isReadOnly()) + { + throw new FolderException("Can't expunge - Permission denied"); + } + + FileInfo messageFileInfo = searchMails().get(uid); + imapService.expungeMessage(messageFileInfo); + } + /** * Returns the MSN number of the first unseen message. * @@ -630,7 +651,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab { try { - result.add(imapService.createImapMessage(fileInfo, false)); + result.add(imapService.createImapMessage(fileInfo, true)); if (logger.isDebugEnabled()) { logger.debug("[convertToMessages] Message added: " + fileInfo.getName()); @@ -744,7 +765,17 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab return null; } }; - command.run(); + try + { + command.run(); + } + catch (AccessDeniedException ade) + { + if (logger.isDebugEnabled()) + { + logger.debug("Access denied to reset RECENT FLAG"); + } + } } return recent; } @@ -769,7 +800,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab @Override public long getUidValidity() { - return getFolderStatus().uidValidity + mountPointId; + return getFolderStatus().uidValidity / 1000L + mountPointId; } /** @@ -812,6 +843,24 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab notifyFlagUpdate(msn, flags, uidNotification, silentListener); } + @Override + protected long[] searchInternal(SearchTerm searchTerm) + { + List messages = getMessages(); + long[] result = new long[messages.size()]; + int i = 0; + + for (SimpleStoredMessage message : messages) + { + if (searchTerm.match(message.getMimeMessage())) + { + result[i] = message.getUid(); + i++; + } + } + return Arrays.copyOfRange(result, 0, i); + } + /** * Sets flags for the message with the given UID. If {@code addUid} is set to {@code true} * {@link FolderListener} objects defined for this folder will be notified. diff --git a/source/java/org/alfresco/repo/imap/ImapService.java b/source/java/org/alfresco/repo/imap/ImapService.java index ad58e0fb8f..bc314484ef 100644 --- a/source/java/org/alfresco/repo/imap/ImapService.java +++ b/source/java/org/alfresco/repo/imap/ImapService.java @@ -310,6 +310,8 @@ public interface ImapService public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage) throws IOException, MessagingException; public String generateUniqueFilename(NodeRef destFolderNodeRef, String fileName); + + public void persistMessageHeaders(NodeRef nodeRef, MimeMessage message); static class FolderStatus { diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index dd2d49023e..ce3338de71 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -22,10 +22,12 @@ import static org.alfresco.repo.imap.AlfrescoImapConst.DICTIONARY_TEMPLATE_PREFI import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -40,6 +42,7 @@ import java.util.TreeMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.mail.Flags; +import javax.mail.Header; import javax.mail.Flags.Flag; import javax.mail.MessagingException; import javax.mail.internet.AddressException; @@ -166,6 +169,8 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea private boolean imapServerEnabled = false; + private List messageHeadersToPersist = Collections.emptyList(); + static { qNameToFlag = new HashMap(); @@ -343,6 +348,11 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea this.imapServerEnabled = enabled; } + public void setMessageHeadersToPersist(List headers) + { + this.messageHeadersToPersist = headers; + } + public boolean getImapServerEnabled() { return this.imapServerEnabled; @@ -574,22 +584,30 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea @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 serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() { - 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; + @Override + public Void execute() throws Throwable + { + // Ignore if it is NOT hidden: the shuffle may have finished; the operation may have failed + if (!nodeService.exists(nodeRef) || !fileFolderService.isHidden(nodeRef)) + { + return null; + } + + // Since this will run in a different thread, the client thread-local must be set + // or else unhiding the node will not unhide it for IMAP. + FileFilterMode.setClient(FileFilterMode.Client.imap); + + // Unhide the node, e.g. for archiving + fileFolderService.setHidden(nodeRef, false); + + // This is the transaction-aware service + fileFolderService.delete(nodeRef); + + return null; + } + }); } }; try @@ -2030,6 +2048,50 @@ public class ImapServiceImpl implements ImapService, OnRestoreNodePolicy, OnCrea return fileName; } + @SuppressWarnings("unchecked") + public void persistMessageHeaders(NodeRef messageRef, MimeMessage message) + { + try + { + Enumeration
headers = message.getAllHeaders(); + List messaheHeadersProperties = new ArrayList(); + while(headers.hasMoreElements()) + { + Header header = headers.nextElement(); + if (isPersistableHeader(header)) + { + messaheHeadersProperties.add(header.getName() + ImapModel.MESSAGE_HEADER_TO_PERSIST_SPLITTER + header.getValue()); + + if (logger.isDebugEnabled()) + { + logger.debug("[persistHeaders] Persisting Header " + header.getName() + " : " + header.getValue()); + } + } + } + + Map props = new HashMap(); + props.put(ImapModel.PROP_MESSAGE_HEADERS, (Serializable)messaheHeadersProperties); + + serviceRegistry.getNodeService().addAspect(messageRef, ImapModel.ASPECT_IMAP_MESSAGE_HEADERS, props); + } + catch(MessagingException me) + { + + } + } + + private boolean isPersistableHeader(Header header) + { + for (String headerToPersist : messageHeadersToPersist) + { + if (headerToPersist.equalsIgnoreCase(header.getName())) + { + return true; + } + } + return false; + } + static class CacheItem { private Date modified; diff --git a/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java b/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java index ad0caea97b..9cddd9aa92 100644 --- a/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java +++ b/source/test-java/org/alfresco/repo/imap/ImapServiceImplTest.java @@ -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.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; @@ -912,13 +913,18 @@ public class ImapServiceImplTest extends TestCase 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); + // original message should be deleted or about to + assertTrue(!nodeService.exists(origFile.getNodeRef()) || imapService.getFlags(origFile).contains(Flags.Flag.DELETED)); + + // new file should be in destination assertEquals("There should be only one node in the destination folder", 1, nodeService.getChildAssocs(destinationNode.getNodeRef()).size()); + NodeRef newNodeRef = nodeService.getChildAssocs(destinationNode.getNodeRef()).get(0).getChildRef(); + FileInfo newNodeFileInfo = fileFolderService.getFileInfo(newNodeRef); + assertEquals("The file name should not change.", fileName, newNodeFileInfo.getName()); + ContentReader reader = contentService.getReader(newNodeRef, ContentModel.PROP_CONTENT); + String contentString = reader.getContentString(); + // new file content should be the same as original one + assertEquals(contentString, nodeContent); } /**