From 52c0d4ddcac8e399db14526ec63758e9305cc98e Mon Sep 17 00:00:00 2001 From: Dave Ward Date: Mon, 10 Oct 2011 12:07:32 +0000 Subject: [PATCH] Merged V3.4-BUG-FIX to HEAD 30947: ALF-10619: Merged PATCHES/V3.1.2 to V3.4-BUG-FIX 30884: ALF-10588: Another possible race condition resulting in out of sync transactions - found on SQL Server and JBoss in 3.1.2 - FTS could process updated and deleted nodes in the same transaction before the tracker got to them, leaving behind the correct transaction ID and deleted nodes but undeleted container docs! - We now have to validate all deletions have been honoured when index tracking 30890: ALF-10588: Temporarily disable FTS during IndexCheckServiceImplTest - Otherwise can get confused by intermediate FTS state of its own nodes! 30894: ALF-10588: Correction to deletion checking - Only search for deleted nodes, not updated ones too! 30948: ALF-10619: Fixed merge issue 30982: - ALF-10503 60k Site Performance: Admin Console | Groups: search with a value that matches all 60 groups: maxClasuesCount=10000 - ALF-10511 60k Site Performance: Admin Console | Users | Edit User | Group Search with a value that matches all 60 groups: maxClauseCount=10000 - ALF-10608 60k Site Performance: Searching for a group to add to a site with a value that matches all 60 groups: maxClauseCount=10000 - ALF-10515 60k Site Performance: Edit Group Display Name: The first time, nothing appears to happen for 10 seconds after pressing [Save] - ALF-10514 60k Site Performance: Admin Console | Groups | Search | Delete Group: no feedback to user for 20 seconds after clicking delete icon 30985: Increases in node, property and aspect caches. 30987: Merged DEV/TEMPORARY to V3.4-BUG-FIX 30984: ALF-9880 : ContentGet web script throws NullPointerException for nodes missing cm:modified property The check for null was added for cm:modified property (similar to BaseDownloadContentServlet). 30995: Fix for ALF-9021 30996: ALF-10324 Cannot disable Home Folder Creation - Bug introduced into V3.1 on the 8 March 2010 - ChainingUserRegistrySynchronizerTest enhanced to check for this - Fix to PersonService: Home folder was not being created for 'missing' persons - PersonService: Changed autoCreate parameters to more descriptive names (okay long) and updated Javadoc 30998: ALF-10512 60k Site Performance: Clicking on Sites (left hand side) in the Repository browser causes transactional limit to be reached - Changed node, aspect, property and parentAssoc cache sizes (based on Derek's Skype message) 31006: ALF-10512 60k Site Performance: Clicking on Sites (left hand side) in the Repository browser causes a transactional limit to be reached - Having changed cache sizes in previous commit, the nodeOwner and acl transactional caches were then blown with test case for ALF-10512 Changed to 20k from 10k. Tried 15k but it still had a problem. 31052: Fix for ALF-10520 Merged HEAD to V3.4-BUG-FIX 31051: Performance improvements for Share Repository browser queries. DB with ~50,000 nodes under Company Home: Before: - I'm Editing - 16 secs, Favorites - 17 secs, Tag - 14 secs After: - I'm Editing - 1.5 secs, Favorites - 1.2 secs, Tag - 1.25 secs 31058: ALF-10324 Cannot disable Home Folder Creation - ChainingUserRegistrySynchronizerTest check using personService with both eager and non eager home folder creation 31064: ALF-9360: Merged PATCHES/V3.4.4 to V3.4-BUG-FIX 30244: Merged DEV/DAVEW/IMAP_NEW to PATCHES/V3.4.4 29635: Rework of IMAP to use lightweight caching and correctly set UIDVALIDITY, NEXTUID and Marked / Unmarked state 29668: 1. Changed get AlfrescoImapFolder.getFullNameInternal to be dynamic for cache support 29692: 1. Reverts changed in AlfrescoImapServer to allow ImapHostManager to be a session key for folder. 2. getFlags relies on FileInfo.getProperties() 29741: 1. Changed AbstractMimeMessage.updateMessageId() to follow RFC2822 (3.6.4. Identification fields) 2. Changed ImapServiceImpl to handle absent folders and return "NO" reply to a client. 3. Changed ImapServiceImpl that behaviours don't fail when Alfresco is being first time bootstrapped with IMAP enabled. 4. Cleared AlfrescoImapFolder constructor. 5. Fixed SelectCommand's response to adhere RFC3501 (6.3.1. SELECT Command) 6. Fixed CommandParser to be able parse the flag which is not surrounded by braces (STORE 2:4 +FLAGS \Deleted) 30235: Completion of IMAP rework - Scalable caching - Proper transactional cache for assembled messages - No more assumption that EHcache will always hold entire folder set at once (and perhaps it can't) - Per session (TCP connection) cache of accessed folders - Session cache validation via a 'change token' that is incremented on all significant events - Folder status attributes evaluated once and reused until the change token changes - Now only changed folders need to be queried on an IMAP sync and the server doesn't have to hold all folders in memory - User's view is consistent with their security permissions - Simplification / overhaul of ImapServiceImpl including efficient recursive path building and matching - AlfrescoImapFolder immutable as it should be - Greenmail fixes - Fixed quoting of mailbox names - Fixed hanging problem in ImapRequestLineReader - regression caused by our 8 bit encoding fix. Avoid using an InputStreamReader to read ISO-8859-1 bytes as it has an internal buffer. 30275: Fix failing IMAP tests broken by my recent refactor! - Fixed greenmail conversion of ISO-8859-1 bytes to chars - Transaction read write attributes on service - Read only commands on AbstractImapFolder - Imap aspect properties must be managed as SYSTEM user - Restored persistence of new mail messages - Avoid unit test txn rollback woes by making it possible to check for existence of a path with FileFolderService 30487: ALF-10268: Merged V3.4-BUG-FIX to PATCHES/V3.4.4 30264: ALF-10187: Merged V3.3 to V3.4-BUG-FIX 30003: ALF-9898: More defensive exception handling to avoid packet pool leaks and extra logging on packet pool exhaustion 30540: ALF-10257: Fixed logic error introduced into Greenmail ImapRequestLineReader 30988: ALF-9361: Merged DEV/DAVEW/IMAP_NEW to PATCHES/V3.4.4 (by Arseny) 30419: Remote test for generic client request sequence. 30547: 1. A bug with FetchCommand particularly with FETCH (BODY.PEEK[1]) with an error 1315912197.789640 1.5 NO FETCH failed. java.lang.String cannot be cast to javax.mail.internet.MimeMultipart This happened while message content is being proceeded like MimeMultipart mp = (MimeMultipart) mimeMessage.getContent();, but javadoc of mimeMessage.getContent() says that this content can be a String in case of non-multipart message. Fixed FetchCommand accordingly to mimeMessage.getContent() javadoc. 2. A bug with RFC822MetadataExtracter When mimeMessage.getHeader("received"); is used with the message with following header Received: with ECARTIS (v1.0.0; list dovecot); Tue, 06 Aug 2002 13:01:17 +0300 (EEST) It doesn't extract a date, because it uses lastReceived.indexOf(';') which returns the position IN the ECARTIS (v1.0.0; list dovecot) after v1.0.0, So it should use lastReceived.lastIndexOf(';') to get the position after ECARTIS (v1.0.0; list dovecot). 31025: ALF-9361: IMAP Performance - Introduced folder status MRU cache - Keyed by user ID and change token so no need to cluster - Now means we should get reuse across IMAP sessions - Also fixed isMarked() implementation to only return true if there are recent or unseen mails 31038: ALF-9361: Prevent the starting of unnecessary transactions in AlfrescoImapFolder interface - getFolderStatus regulates its own transaction - Dropped all those *Internal methods from the abstract class - getUnqualifiedMailboxPattern moved to AlfrescoImapHostManager - Fixes to session folder cache validation / reuse 31039: ALF-9361: Repository tuning for IMAP performance - Backed out ALF-5575 60 second timeout on node caches - Should be covered by ALF-8607 fix - Also made TransactionalCache.NewCacheBucket save new values to the shared cache for 'mutable' caches. Previously it was only possibly to load into the node caches in a read only transaction! - Also added fix to make AbstractNodeDAOImpl bulk load empty node aspect sets - Result is a drastic speedup of full sync times as most items can be served from the cache` 31042: ALF-9361: Fix ImapServiceImplTest 31048: ALF-9361: Make ConcurrentNodeServiceTest work again, after relaxation of 'mutable' transactional caches - aspect and property caches validated by node transaction ID, as per parent assocs in ALF-8607 31050: ALF-9361: Caching correction Always use the cached mailbox reference if it is equivalent (because the session remembers the last selected mailbox) 31060: ALF-9361: Fix CacheTest, following back out of ALF-5575 behaviour 31061: ALF-9361: More caching fixes 31062: ALF-9361: Undo accidental changes to ConcurrentNodeServiceTest 31063: ALF-9361: Build fix: replaced assertSame with assertEquals git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@31079 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/cache-context.xml | 44 +- config/alfresco/ehcache-default.xml | 28 +- .../ehcache-custom.xml.sample.cluster | 32 +- config/alfresco/imap/scripts/command-utils.js | 4 +- config/alfresco/model/imapModel.xml | 17 +- .../imap/default/imap-server-context.xml | 64 +- .../imap/default/imap-server.properties | 1 + source/java/org/alfresco/model/ImapModel.java | 2 + .../org/alfresco/repo/cache/CacheTest.java | 6 +- .../repo/cache/TransactionalCache.java | 6 +- .../metadata/RFC822MetadataExtracter.java | 2 +- .../repo/imap/AbstractImapFolder.java | 229 --- .../repo/imap/AbstractMimeMessage.java | 11 +- .../repo/imap/AlfrescoImapFolder.java | 841 ++------ .../repo/imap/AlfrescoImapHostManager.java | 225 +- .../repo/imap/AlfrescoImapServer.java | 17 +- .../alfresco/repo/imap/AlfrescoImapUser.java | 2 +- .../repo/imap/ContentModelMessage.java | 6 +- .../alfresco/repo/imap/ImapModelMessage.java | 26 +- .../org/alfresco/repo/imap/ImapService.java | 112 +- .../alfresco/repo/imap/ImapServiceImpl.java | 1803 ++++++----------- .../repo/imap/ImapServiceImplCacheTest.java | 51 +- .../repo/imap/ImapServiceImplTest.java | 71 +- .../org/alfresco/repo/imap/LoadTester.java | 8 +- .../alfresco/repo/imap/RemoteLoadTester.java | 77 +- .../java/org/alfresco/repo/jscript/Imap.java | 6 +- .../filefolder/FileFolderServiceImpl.java | 16 +- .../node/index/AbstractReindexComponent.java | 34 +- .../security/authority/AuthorityDAOImpl.java | 50 +- .../script/ScriptAuthorityService.java | 5 +- .../security/person/PersonServiceImpl.java | 32 +- .../ChainingUserRegistrySynchronizer.java | 2 +- .../ChainingUserRegistrySynchronizerTest.java | 48 +- .../service/cmr/model/FileFolderService.java | 11 + .../service/cmr/security/PersonService.java | 32 +- 35 files changed, 1467 insertions(+), 2454 deletions(-) diff --git a/config/alfresco/cache-context.xml b/config/alfresco/cache-context.xml index 6f914a2fbc..c1bcea9a32 100644 --- a/config/alfresco/cache-context.xml +++ b/config/alfresco/cache-context.xml @@ -205,7 +205,7 @@ org.alfresco.cache.node.nodesTransactionalCache - + @@ -238,7 +238,7 @@ org.alfresco.cache.node.aspectsTransactionalCache - + @@ -271,7 +271,7 @@ org.alfresco.cache.node.propertiesTransactionalCache - + @@ -304,7 +304,7 @@ org.alfresco.cache.node.parentAssocsTransactionalCache - + @@ -586,7 +586,7 @@ org.alfresco.nodeOwnerTransactionalCache - + @@ -859,7 +859,7 @@ org.alfresco.aclTransactionalCache - + @@ -1243,5 +1243,37 @@ + + + + + + + + + + + + + + org.alfresco.cache.imapMessageCache + + + + + + + + + + + + + org.alfresco.cache.imapMessageTransactionalCache + + + + + diff --git a/config/alfresco/ehcache-default.xml b/config/alfresco/ehcache-default.xml index 429564cbf4..9890c62d23 100644 --- a/config/alfresco/ehcache-default.xml +++ b/config/alfresco/ehcache-default.xml @@ -51,33 +51,29 @@ /> @@ -206,7 +202,7 @@ - + @@ -144,10 +144,10 @@ @@ -163,10 +163,10 @@ @@ -182,10 +182,8 @@ @@ -474,7 +472,7 @@ - + d:boolean - - + + + UIDVALIDITY d:long - + + + MAXUID + d:long + + + CHANGETOKEN + d:text + - + diff --git a/config/alfresco/subsystems/imap/default/imap-server-context.xml b/config/alfresco/subsystems/imap/default/imap-server-context.xml index e1e16ad96a..f6ff15241f 100644 --- a/config/alfresco/subsystems/imap/default/imap-server-context.xml +++ b/config/alfresco/subsystems/imap/default/imap-server-context.xml @@ -19,8 +19,8 @@ ${imap.server.enabled} - - + + @@ -84,49 +84,11 @@ - ${server.transaction.mode.readOnly} - ${server.transaction.mode.default} - ${server.transaction.mode.default} - ${server.transaction.mode.readOnly} ${server.transaction.mode.default} - - - - - - - - - org.alfresco.cache.imapFoldersCache - - - - - - - - - - - - - - org.alfresco.repo.cache.SimpleCache - - - - - - - - - - - @@ -134,8 +96,8 @@ - - + + @@ -148,9 +110,8 @@ - - - + + @@ -174,6 +135,9 @@ + + ${imap.server.folder.cache.size} + ${imap.server.enabled} @@ -197,16 +161,6 @@ - - - - - - - - - - diff --git a/config/alfresco/subsystems/imap/default/imap-server.properties b/config/alfresco/subsystems/imap/default/imap-server.properties index 36d5aa6a2a..73f046d6a5 100644 --- a/config/alfresco/subsystems/imap/default/imap-server.properties +++ b/config/alfresco/subsystems/imap/default/imap-server.properties @@ -1,6 +1,7 @@ imap.server.enabled=false imap.server.port=143 imap.server.host=0.0.0.0 +imap.server.folder.cache.size=10000 imap.mail.from.default=alfresco@demo.alfresco.org imap.mail.to.default=alfresco@demo.alfresco.org diff --git a/source/java/org/alfresco/model/ImapModel.java b/source/java/org/alfresco/model/ImapModel.java index dcf0144a42..63da60ed04 100644 --- a/source/java/org/alfresco/model/ImapModel.java +++ b/source/java/org/alfresco/model/ImapModel.java @@ -57,5 +57,7 @@ public interface ImapModel static final QName PROP_ATTACH_ID = QName.createQName(IMAP_MODEL_1_0_URI, "attachID"); static final QName PROP_UIDVALIDITY = QName.createQName(IMAP_MODEL_1_0_URI, "uidValidity"); + static final QName PROP_MAXUID = QName.createQName(IMAP_MODEL_1_0_URI, "maxUid"); + static final QName PROP_CHANGE_TOKEN = QName.createQName(IMAP_MODEL_1_0_URI, "changeToken"); } diff --git a/source/java/org/alfresco/repo/cache/CacheTest.java b/source/java/org/alfresco/repo/cache/CacheTest.java index b41ff5a2bf..621dd0e64e 100644 --- a/source/java/org/alfresco/repo/cache/CacheTest.java +++ b/source/java/org/alfresco/repo/cache/CacheTest.java @@ -240,11 +240,11 @@ public class CacheTest extends TestCase // check that backing cache was updated with the in-transaction changes assertFalse("Item was not removed from backing cache", backingCache.contains(NEW_GLOBAL_ONE)); assertNull("Item could still be fetched from backing cache", backingCache.get(NEW_GLOBAL_ONE)); - assertEquals("Item not updated in backing cache", null, backingCache.get(UPDATE_TXN_THREE)); + assertEquals("Item not updated in backing cache", "XXX", backingCache.get(UPDATE_TXN_THREE)); // Check that the transactional cache serves get requests - assertEquals("Transactional cache must serve post-commit get requests", - null, transactionalCache.get(UPDATE_TXN_THREE)); + assertEquals("Transactional cache must serve post-commit get requests", "XXX", + transactionalCache.get(UPDATE_TXN_THREE)); } catch (Throwable e) { diff --git a/source/java/org/alfresco/repo/cache/TransactionalCache.java b/source/java/org/alfresco/repo/cache/TransactionalCache.java index 2486df6dfb..50834f1c03 100644 --- a/source/java/org/alfresco/repo/cache/TransactionalCache.java +++ b/source/java/org/alfresco/repo/cache/TransactionalCache.java @@ -856,7 +856,11 @@ public class TransactionalCache else { // Mutable, read-write - if (sharedObj != null) + if (sharedObj == null) + { + sharedCache.put(key, value); + } + else { // Remove new value in the cache sharedCache.remove(key); diff --git a/source/java/org/alfresco/repo/content/metadata/RFC822MetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/RFC822MetadataExtracter.java index 22ea98ea24..d9a482cdb8 100644 --- a/source/java/org/alfresco/repo/content/metadata/RFC822MetadataExtracter.java +++ b/source/java/org/alfresco/repo/content/metadata/RFC822MetadataExtracter.java @@ -134,7 +134,7 @@ public class RFC822MetadataExtracter extends AbstractMappingMetadataExtracter { String lastReceived = rx[0]; lastReceived = MimeUtility.unfold(lastReceived); - int x = lastReceived.indexOf(';'); + int x = lastReceived.lastIndexOf(';'); if(x > 0) { String dateStr = lastReceived.substring(x + 1).trim(); diff --git a/source/java/org/alfresco/repo/imap/AbstractImapFolder.java b/source/java/org/alfresco/repo/imap/AbstractImapFolder.java index 558489374b..a823e10d0c 100644 --- a/source/java/org/alfresco/repo/imap/AbstractImapFolder.java +++ b/source/java/org/alfresco/repo/imap/AbstractImapFolder.java @@ -161,37 +161,6 @@ public abstract class AbstractImapFolder implements MailFolder } - /** - * Returns the number of the first unseen message. - * - * @return Number of the first unseen message. - */ - public int getFirstUnseen() - { - return getFirstUnseenInternal(); - } - - - /** - * Returns full name of the folder with namespace and full path delimited with the hierarchy delimiter - * (see {@link AlfrescoImapConst#HIERARCHY_DELIMITER}) - *

E.g.:
- * #mail.admin."Repository_archive.Data Dictionary.Space Templates.Software Engineering Project"
- * This is required by GreenMail implementation. - */ - public String getFullName() - { - CommandCallback command = new CommandCallback() - { - public String command() throws Throwable - { - return getFullNameInternal(); - } - }; - return command.run(); - } - - /** * Returns message by its UID. * @@ -210,23 +179,6 @@ public abstract class AbstractImapFolder implements MailFolder return command.run(); } - /** - * Returns count of the messages in the folder. - * - * @return Count of the messages. - */ - public int getMessageCount() - { - CommandCallback command = new CommandCallback() - { - public Integer command() throws Throwable - { - return getMessageCountInternal(); - } - }; - return command.run(); - } - /** * Returns list of all messages in the folder. * @@ -262,59 +214,6 @@ public abstract class AbstractImapFolder implements MailFolder return command.run(); } - /** - * Returns message sequence number in the folder by its UID. - * - * @param uid - message UID. - * @return message sequence number. - * @throws FolderException if no message with given UID. - */ - public int getMsn(final long uid) throws FolderException - { - CommandCallback command = new CommandCallback() - { - public Integer command() throws Throwable - { - return getMsnInternal(uid); - } - }; - return command.runFeedback(true); - } - - /** - * Returns folder name. - * - * @return folder name. - */ - public String getName() - { - CommandCallback command = new CommandCallback() - { - public String command() throws Throwable - { - return getNameInternal(); - } - }; - return command.run(); - } - - /** - * Returns UIDs of all messages in the folder. - * - * @return UIDS of the messages. - */ - public long[] getMessageUids() - { - CommandCallback command = new CommandCallback() - { - public Object command() throws Throwable - { - return getMessageUidsInternal(); - } - }; - return (long[])command.run(); - } - /** * Returns the list of messages that have no {@link Flags.Flag#DELETED} flag set for current user. * @@ -333,110 +232,6 @@ public abstract class AbstractImapFolder implements MailFolder return result; } - /** - * Returns permanent flags. - * - * @return {@link Flags} object containing flags. - */ - public Flags getPermanentFlags() - { - CommandCallback command = new CommandCallback() - { - public Flags command() throws Throwable - { - return getPermanentFlagsInternal(); - } - }; - return command.run(true); - } - - /** - * Returns count of messages with {@link Flags.Flag#RECENT} flag. If {@code reset} parameter is {@code true} - - * removes {@link Flags.Flag#RECENT} flag from the message for current user. - * - * @param reset - if true the {@link Flags.Flag#RECENT} will be deleted for current user if exists. - * @return returns count of recent messages. - */ - public int getRecentCount(final boolean reset) - { - CommandCallback command = new CommandCallback() - { - public Integer command() throws Throwable - { - return getRecentCountInternal(reset); - } - }; - return command.run(); - } - - /** - * Returns UIDNEXT value of the folder. - * - * @return UIDNEXT value. - */ - public long getUidNext() - { - CommandCallback command = new CommandCallback() - { - public Long command() throws Throwable - { - return getUidNextInternal(); - } - }; - return command.run(false); - } - - /** - * Returns UIDVALIDITY value of the folder. - * - * @return UIDVALIDITY value. - */ - public long getUidValidity() - { - CommandCallback command = new CommandCallback() - { - public Long command() throws Throwable - { - return getUidValidityInternal(); - } - }; - return command.run(false); - } - - /** - * Returns count of the messages with {@link Flags.Flag#SEEN} in the folder for the current user. - * - * @return Count of the unseen messages for current user. - */ - public int getUnseenCount() - { - CommandCallback command = new CommandCallback() - { - public Integer command() throws Throwable - { - return getUnseenCountInternal(); - } - }; - return command.run(); - } - - /** - * Whether the folder is selectable. - * - * @return {@code boolean}. - */ - public boolean isSelectable() - { - CommandCallback command = new CommandCallback() - { - public Boolean command() throws Throwable - { - return isSelectableInternal(); - } - }; - return command.run(true); - } - /** * Replaces 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. @@ -587,38 +382,14 @@ public abstract class AbstractImapFolder implements MailFolder protected abstract void expungeInternal() throws Exception; - protected abstract int getFirstUnseenInternal(); - - protected abstract String getFullNameInternal() throws Exception; - protected abstract SimpleStoredMessage getMessageInternal(long uid) throws Exception; - protected abstract int getMessageCountInternal(); - protected abstract List getMessagesInternal(); protected abstract List getMessagesInternal(MsgRangeFilter msgRangeFilter); - protected abstract int getMsnInternal(long uid) throws Exception; - - protected abstract String getNameInternal(); - - protected abstract long[] getMessageUidsInternal(); - protected abstract List getNonDeletedMessagesInternal(); - protected abstract Flags getPermanentFlagsInternal(); - - protected abstract int getRecentCountInternal(boolean reset); - - protected abstract long getUidNextInternal(); - - protected abstract long getUidValidityInternal(); - - protected abstract int getUnseenCountInternal(); - - protected abstract boolean isSelectableInternal(); - protected abstract void replaceFlagsInternal(Flags flags, long uid, FolderListener silentListener, boolean addUid) throws Exception; protected abstract void setFlagsInternal(Flags flags, boolean value, long uid, FolderListener silentListener, boolean addUid) throws Exception; diff --git a/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java b/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java index 144a1fef9c..0061043aa1 100644 --- a/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java +++ b/source/java/org/alfresco/repo/imap/AbstractMimeMessage.java @@ -21,24 +21,18 @@ package org.alfresco.repo.imap; import static org.alfresco.repo.imap.AlfrescoImapConst.MIME_VERSION; import static org.alfresco.repo.imap.AlfrescoImapConst.X_ALF_NODEREF_ID; -import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Properties; -import javax.mail.Address; import javax.mail.Flags; import javax.mail.MessagingException; import javax.mail.Session; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; -import org.springframework.extensions.surf.util.I18NUtil; import org.alfresco.model.ContentModel; import org.alfresco.repo.imap.ImapService.EmailBodyFormat; -import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.site.SiteModel; import org.alfresco.repo.template.TemplateNode; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -47,7 +41,6 @@ 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.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -56,6 +49,8 @@ import org.apache.commons.logging.LogFactory; */ public abstract class AbstractMimeMessage extends MimeMessage { + protected static final String DEFAULT_SUFFIX = "@alfresco.org"; + protected static int MAX_RETRIES = 1; private Log logger = LogFactory.getLog(AbstractMimeMessage.class); @@ -238,6 +233,6 @@ public abstract class AbstractMimeMessage extends MimeMessage protected void updateMessageID() throws MessagingException { - setHeader("Message-ID", this.messageFileInfo.getNodeRef().getId()); + setHeader("Message-ID", "<" + this.messageFileInfo.getNodeRef().getId() + DEFAULT_SUFFIX + ">"); } } diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapFolder.java index 5355be1caa..68c48c1f36 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-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -19,52 +19,38 @@ package org.alfresco.repo.imap; import java.io.IOException; -import java.io.OutputStream; import java.io.Serializable; -import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.TreeMap; +import java.util.NavigableMap; import javax.mail.Flags; import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.Part; -import javax.mail.internet.ContentType; +import javax.mail.Flags.Flag; import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeUtility; import org.alfresco.model.ContentModel; import org.alfresco.model.ImapModel; import org.alfresco.repo.imap.AlfrescoImapConst.ImapViewMode; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.imap.ImapService.FolderStatus; 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.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; import org.alfresco.util.Utf7; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.util.FileCopyUtils; import com.icegreen.greenmail.foedus.util.MsgRangeFilter; -import com.icegreen.greenmail.imap.ImapConstants; import com.icegreen.greenmail.store.FolderException; import com.icegreen.greenmail.store.FolderListener; import com.icegreen.greenmail.store.MailFolder; @@ -75,101 +61,51 @@ import com.icegreen.greenmail.store.SimpleStoredMessage; * Implementation of greenmail MailFolder. It represents an Alfresco content folder and handles * appendMessage, copyMessage, expunge (delete), getMessages, getMessage and so requests. * - * The folder is identified by a qualifiedMailboxName and versioned with a version number called UIDVALIDITY. - * * @author Mike Shavnev + * @author David Ward */ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializable { - /** - * - */ private static final long serialVersionUID = -7223111284066976111L; - private final static long YEAR_2005 = 1101765600000L; - private static Log logger = LogFactory.getLog(AlfrescoImapFolder.class); /** * Reference to the {@link FileInfo} object representing the folder. */ - private FileInfo folderInfo; - - /** - * Reference to the root node of the store where folder is placed. - */ - private NodeRef rootNodeRef; - - /** - * Name of the mailbox (e.g. "admin" for admin user). - */ - private String qualifiedMailboxName; + private final FileInfo folderInfo; /** * Name of the folder. */ - private String folderName; + private final String folderName; + + private final String folderPath; + + private final String userName; /** * Defines view mode. */ - private ImapViewMode viewMode; - - /** - * Name of the mount point. - */ - private String mountPointName; + private final ImapViewMode viewMode; /** * Reference to the {@link ImapService} object. */ - private ImapService imapService; - + private final ImapService imapService; + /** * Defines whether the folder is selectable or not. */ - private boolean selectable; + private final boolean selectable; + private final boolean extractAttachmentsEnabled; + /** - * The UIDValidity + * Cached Folder status information, validated against a change token. */ - private long uidValidity = 0; + private FolderStatus folderStatus; - private boolean extractAttachmentsEnabled; - - private Map messages = new TreeMap(); - private Map msnCache = new HashMap(); - private Map messagesCache = Collections.synchronizedMap(new MaxSizeMap(10, MESSAGE_CACHE_SIZE)); - - /** - * Map that ejects the last recently used element(s) to keep the size to a - * specified maximum - * - * @param Key - * @param Value - */ - private class MaxSizeMap extends LinkedHashMap - { - private static final long serialVersionUID = 1L; - - private int maxSize; - - public MaxSizeMap(int initialSize, int maxSize) - { - super(initialSize, 0.75f, true); - this.maxSize = maxSize; - } - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) - { - boolean remove = super.size() > this.maxSize; - return remove; - } - } - - private final static int MESSAGE_CACHE_SIZE = 40; - private static final Flags PERMANENT_FLAGS = new Flags(); static @@ -186,11 +122,15 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab return extractAttachmentsEnabled; } - /*package*/ AlfrescoImapFolder(String qualifiedMailboxName, ServiceRegistry serviceRegistry) + /** + * Protected constructor for the hierarchy delimiter + */ + AlfrescoImapFolder(String userName, ServiceRegistry serviceRegistry) { - this(qualifiedMailboxName, null, null, null, null, null, false, serviceRegistry); + this(null, userName, "", "", null, serviceRegistry, false, false); } - + + /** * Constructs {@link AlfrescoImapFolder} object. * @@ -202,16 +142,15 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @param mountPointName - name of the mount point. */ public AlfrescoImapFolder( - String qualifiedMailboxName, FileInfo folderInfo, + String userName, String folderName, + String folderPath, ImapViewMode viewMode, - NodeRef rootNodeRef, - String mountPointName, boolean extractAttachmentsEnabled, ServiceRegistry serviceRegistry) { - this(qualifiedMailboxName, folderInfo, folderName, viewMode, rootNodeRef, mountPointName, serviceRegistry, null, extractAttachmentsEnabled); + this(folderInfo, userName, folderName, folderPath, viewMode, serviceRegistry, null, extractAttachmentsEnabled); } /** @@ -227,32 +166,24 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @param selectable - defines whether the folder is selectable or not. */ public AlfrescoImapFolder( - String qualifiedMailboxName, FileInfo folderInfo, + String userName, String folderName, + String folderPath, ImapViewMode viewMode, - NodeRef rootNodeRef, - String mountPointName, ServiceRegistry serviceRegistry, Boolean selectable, boolean extractAttachmentsEnabled) { super(serviceRegistry); - this.qualifiedMailboxName = qualifiedMailboxName; this.folderInfo = folderInfo; - this.rootNodeRef = rootNodeRef; + this.userName = userName; this.folderName = folderName != null ? folderName : (folderInfo != null ? folderInfo.getName() : null); + this.folderPath = folderPath; this.viewMode = viewMode != null ? viewMode : ImapViewMode.ARCHIVE; - this.mountPointName = mountPointName; this.extractAttachmentsEnabled = extractAttachmentsEnabled; - - if (serviceRegistry != null) - { - this.imapService = serviceRegistry.getImapService(); - } + this.imapService = serviceRegistry.getImapService(); - this.uidValidity = generateUidValidity(); - // MailFolder object can be null if it is used to obtain hierarchy delimiter by LIST command: // Example: // C: 2 list "" "" @@ -266,24 +197,96 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab Boolean storedSelectable = !serviceRegistry.getNodeService().hasAspect(folderInfo.getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSELECTABLE); if (storedSelectable == null) { - setSelectable(true); + this.selectable = true; } else { - setSelectable(storedSelectable); + this.selectable = storedSelectable; } } else { - setSelectable(selectable); + this.selectable = selectable; } } else { - setSelectable(true); + this.selectable = false; } } + + /* + * (non-Javadoc) + * @see com.icegreen.greenmail.store.MailFolder#getFullName() + */ + @Override + public String getFullName() + { + return Utf7.encode(AlfrescoImapConst.USER_NAMESPACE + AlfrescoImapConst.HIERARCHY_DELIMITER + this.userName + + AlfrescoImapConst.HIERARCHY_DELIMITER + getFolderPath(), Utf7.UTF7_MODIFIED); + } + /* (non-Javadoc) + * @see com.icegreen.greenmail.store.MailFolder#getName() + */ + @Override + public String getName() + { + return this.folderName; + } + + /* (non-Javadoc) + * @see com.icegreen.greenmail.store.MailFolder#isSelectable() + */ + @Override + public boolean isSelectable() + { + return this.selectable; + } + + /** + * Returns the contents of this folder. + * + * @return A sorted map of UIDs to FileInfo objects. + */ + private NavigableMap searchMails() + { + return getFolderStatus().search; + } + + /** + * Invalidates the current cached state + * + * @return true if this instance is still valid for reuse + */ + public boolean reset() + { + this.folderStatus = null; + return new CommandCallback() + { + public Boolean command() throws Throwable + { + return serviceRegistry.getNodeService().exists(folderInfo.getNodeRef()); + } + }.run(true); + } + + protected FolderStatus getFolderStatus() + { + if (this.folderStatus == null) + { + CommandCallback command = new CommandCallback() + { + public FolderStatus command() throws Throwable + { + return imapService.getFolderStatus(userName, folderInfo.getNodeRef(), viewMode); + } + }; + this.folderStatus = command.run(); + } + return this.folderStatus; + } + /** * Appends message to the folder. * @@ -298,15 +301,10 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab Date internalDate) throws FileExistsException, FileNotFoundException, IOException, MessagingException { - AbstractMimeMessage internalMessage = createMimeMessageInFolder(this.folderInfo, message); - long newMessageUid = (Long) internalMessage.getMessageInfo().getProperties().get(ContentModel.PROP_NODE_DBID); - SimpleStoredMessage storedMessage = new SimpleStoredMessage(internalMessage, new Date(), newMessageUid); - messages.put(newMessageUid, storedMessage); - - // Saving message sequence number to cache - msnCache.put(newMessageUid, messages.size()); - - return newMessageUid; + long uid = createMimeMessageInFolder(this.folderInfo, message, flags); + // Invalidate current folder status + this.folderStatus = null; + return uid; } /** @@ -328,21 +326,18 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab NodeRef destFolderNodeRef = toImapMailFolder.getFolderInfo().getNodeRef(); - SimpleStoredMessage message = messages.get(uid); - FileInfo sourceMessageFileInfo = ((AbstractMimeMessage) message.getMimeMessage()).getMessageInfo(); + FileInfo sourceMessageFileInfo = searchMails().get(uid); if (serviceRegistry.getNodeService().hasAspect(sourceMessageFileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) { //Generate body of message MimeMessage newMessage = new ImapModelMessage(sourceMessageFileInfo, serviceRegistry, true); - toImapMailFolder.appendMessageInternal(newMessage, message.getFlags(), new Date()); + toImapMailFolder.appendMessageInternal(newMessage, imapService.getFlags(sourceMessageFileInfo), new Date()); } else { serviceRegistry.getFileFolderService().copy(sourceMessageFileInfo.getNodeRef(), destFolderNodeRef, null); } - removeMessageFromCache(uid); - } /** @@ -356,14 +351,11 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab throw new FolderException("Can't delete all - Permission denied"); } - for (SimpleStoredMessage mess : messages.values()) + for (Map.Entry entry : searchMails().entrySet()) { - AbstractMimeMessage message = (AbstractMimeMessage) mess.getMimeMessage(); - FileInfo fileInfo = message.getMessageInfo(); - imapService.setFlag(fileInfo, Flags.Flag.DELETED, true); + imapService.setFlag(entry.getValue(), Flags.Flag.DELETED, true); // comment out to physically remove content. // fileFolderService.delete(fileInfo.getNodeRef()); - removeMessageFromCache(mess.getUid()); } } @@ -378,92 +370,21 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab throw new FolderException("Can't expunge - Permission denied"); } - Collection listMess = messages.values(); - for (SimpleStoredMessage mess : listMess) + for (Map.Entry entry : searchMails().entrySet()) { - - Flags flags = getFlags(mess); - if (flags.contains(Flags.Flag.DELETED)) - { - NodeRef nodeRef = ((AbstractMimeMessage) mess.getMimeMessage()).getMessageInfo().getNodeRef(); - serviceRegistry.getFileFolderService().delete(nodeRef); - removeMessageFromCache(mess.getUid()); - } + imapService.expungeMessage(entry.getValue()); } } /** - * Returns the number of the first unseen message. + * Returns the MSN number of the first unseen message. * - * @return Number of the first unseen message. + * @return MSN number of the first unseen message. */ @Override - protected int getFirstUnseenInternal() + public int getFirstUnseen() { - return 0; - } - - /** - * Returns full name of the folder with namespace and full path delimited with the hierarchy delimiter - * (see {@link AlfrescoImapConst#HIERARCHY_DELIMITER})

- * E.g.:
- * #mail.admin."Repository_archive.Data Dictionary.Space Templates.Software Engineering Project"
- * This is required by GreenMail implementation. - * - * @throws FileNotFoundException - */ - @Override - protected String getFullNameInternal() throws FileNotFoundException - { - // If MailFolder object is used to obtain hierarchy delimiter by LIST command: - // Example: - // C: 2 list "" "" - // S: * LIST () "." "" - // S: 2 OK LIST completed. - - if (rootNodeRef == null) - { - return ""; - } - - if (logger.isDebugEnabled()) - { - logger.debug("[getFullNameInternal] " + this); - } - - StringBuilder fullName = new StringBuilder(); - List pathList; - pathList = serviceRegistry.getFileFolderService().getNamePath(rootNodeRef, folderInfo.getNodeRef()); - fullName.append(ImapConstants.USER_NAMESPACE).append(AlfrescoImapConst.HIERARCHY_DELIMITER).append(qualifiedMailboxName); - - boolean isFirst = true; - for (FileInfo path : pathList) - { - fullName.append(AlfrescoImapConst.HIERARCHY_DELIMITER); - if (isFirst) - { - fullName.append("\""); - isFirst = false; - if (mountPointName != null) - { - fullName.append(mountPointName); - } - else - { - fullName.append(path.getName()); - } - } - else - { - fullName.append(path.getName()); - } - } - fullName.append("\""); - if (logger.isDebugEnabled()) - { - logger.debug("fullName: " + fullName); - } - return Utf7.encode(fullName.toString(), Utf7.UTF7_MODIFIED); + return getFolderStatus().firstUnseen; } /** @@ -480,46 +401,12 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab { logger.debug("[getMessageInternal] " + this); } - SimpleStoredMessage storedMessage = messages.get(uid); - if (storedMessage == null) + FileInfo mesInfo = searchMails().get(uid); + if (mesInfo == null) { - messagesCache.remove(uid); - msnCache.remove(uid); return null; } - AbstractMimeMessage mes = (AbstractMimeMessage) storedMessage.getMimeMessage(); - FileInfo mesInfo = mes.getMessageInfo(); - - Date modified = (Date) serviceRegistry.getNodeService().getProperty(mesInfo.getNodeRef(), ContentModel.PROP_MODIFIED); - if(modified != null) - { - CacheItem cached = messagesCache.get(uid); - if (cached != null) - { - if (logger.isDebugEnabled()) - { - logger.debug("retrieved message from cache uid: " + uid); - } - if (cached.getModified().equals(modified)) - { - return cached.getMessage(); - } - } - SimpleStoredMessage message = createImapMessage(mesInfo, uid, true); - messagesCache.put(uid, new CacheItem(modified, message)); - - if (logger.isDebugEnabled()) - { - logger.debug("caching message uid: " + uid + " cacheSize: " + messagesCache.size()); - } - - return message; - } - else - { - SimpleStoredMessage message = createImapMessage(mesInfo, uid, true); - return message; - } + return imapService.getMessage(mesInfo); } /** @@ -528,23 +415,9 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @return Count of the messages. */ @Override - protected int getMessageCountInternal() + public int getMessageCount() { - if (logger.isDebugEnabled()) - { - logger.debug("[getMessageCountInternal] " + this); - } - - if (messages.size() == 0 && folderInfo != null) - { - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - convertToMessages(fileInfos); - } - if (logger.isDebugEnabled() && folderInfo != null) - { - logger.debug(folderInfo.getName() + " - Messages count:" + messages.size()); - } - return messages.size(); + return getFolderStatus().messageCount; } /** @@ -553,25 +426,19 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @return UIDS of the messages. */ @Override - protected long[] getMessageUidsInternal() + public long[] getMessageUids() { if (logger.isDebugEnabled()) { logger.debug("[getMessageUidsInternal] " + this); } - if (messages == null || messages.size() == 0 && folderInfo != null) - { - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - convertToMessages(fileInfos); - } - int len = messages.size(); - long[] uids = new long[len]; - Set keys = messages.keySet(); + Collection uidSet = searchMails().keySet(); + long[] uids = new long[uidSet.size()]; int i = 0; - for (Long key : keys) + for (Long uid : uidSet) { - uids[i++] = key; + uids[i++] = uid; } return uids; } @@ -588,11 +455,10 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab { logger.debug("[getMessagesInternal] " + this); } - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - return convertToMessages(fileInfos); + return convertToMessages(searchMails().values()); } - private List convertToMessages(List fileInfos) + private List convertToMessages(Collection fileInfos) { if (logger.isDebugEnabled()) { @@ -603,44 +469,23 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab logger.debug("[convertToMessages] - fileInfos is empty or null"); return Collections.emptyList(); } - if (fileInfos.size() != messages.size()) + List result = new LinkedList(); + for (FileInfo fileInfo : fileInfos) { - for (FileInfo fileInfo : fileInfos) + try { - try + result.add(imapService.createImapMessage(fileInfo, false)); + if (logger.isDebugEnabled()) { - Long key = getMessageUid(fileInfo); - SimpleStoredMessage message = createImapMessage(fileInfo, key, false); - messages.put(key, message); - - // Saving message sequence number to cache - msnCache.put(key, messages.size()); - - if (logger.isDebugEnabled()) - { - logger.debug("[convertToMessages] Message added: " + fileInfo.getName()); - } - } - catch (MessagingException e) - { - logger.warn("[convertToMessages] Invalid message! File name:" + fileInfo.getName(), e); + logger.debug("[convertToMessages] Message added: " + fileInfo.getName()); } } + catch (MessagingException e) + { + logger.warn("[convertToMessages] Invalid message! File name:" + fileInfo.getName(), e); + } } - return new LinkedList(messages.values()); - } - - protected SimpleStoredMessage createImapMessage(FileInfo fileInfo, Long key, 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 - if (serviceRegistry.getNodeService().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); - } + return result; } /** @@ -652,25 +497,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab @Override protected List getMessagesInternal(MsgRangeFilter msgRangeFilter) { - if (logger.isDebugEnabled()) - { - logger.debug("[getMessagesInternal] " + this); - } - if (messages == null || messages.size() == 0) - { - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - convertToMessages(fileInfos); - } - List ret = new ArrayList(); - for (int i = 0; i < messages.size(); i++) - { - if (msgRangeFilter.includes(i + 1)) - { - ret.add(messages.get(i)); - } - } - - return ret; + throw new UnsupportedOperationException("IMAP implementation doesn't support POP3 requests"); } /** @@ -681,25 +508,14 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @throws FolderException if no message with given UID. */ @Override - protected int getMsnInternal(long uid) throws FolderException + public int getMsn(long uid) throws FolderException { - Integer msn = msnCache.get(uid); - if (msn != null) + NavigableMap messages = searchMails(); + if (!messages.containsKey(uid)) { - return msn; + throw new FolderException("No such message."); } - throw new FolderException("No such message."); - } - - /** - * Returns folder name. - * - * @return folder name. - */ - @Override - protected String getNameInternal() - { - return folderName; + return messages.headMap(uid, true).size(); } /** @@ -716,13 +532,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab } List result = new ArrayList(); - if (messages.size() == 0 && folderInfo != null) - { - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - convertToMessages(fileInfos); - } - - Collection values = messages.values(); + Collection values = getMessagesInternal(); for (SimpleStoredMessage message : values) { if (!getFlags(message).contains(Flags.Flag.DELETED)) @@ -744,7 +554,7 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @return {@link Flags} object containing flags. */ @Override - protected Flags getPermanentFlagsInternal() + public Flags getPermanentFlags() { return PERMANENT_FLAGS; } @@ -758,38 +568,29 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @return returns count of recent messages. */ @Override - protected int getRecentCountInternal(boolean reset) + public int getRecentCount(boolean reset) { - if (logger.isDebugEnabled()) + int recent = getFolderStatus().recentCount; + if (reset && recent > 0) { - logger.debug("[getRecentCountInternal] " + this); - } - if (messages.size() == 0 && folderInfo != null) - { - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - convertToMessages(fileInfos); - } - - int count = 0; - Collection values = messages.values(); - for (SimpleStoredMessage message : values) - { - if (getFlags(message).contains(Flags.Flag.RECENT)) + CommandCallback command = new CommandCallback() { - count++; - if (reset) + public Void command() throws Throwable { - imapService.setFlag(((AbstractMimeMessage) message.getMimeMessage()).getMessageInfo(), Flags.Flag.RECENT, false); + for (FileInfo fileInfo : folderStatus.search.values()) + { + Flags flags = imapService.getFlags(fileInfo); + if (flags.contains(Flags.Flag.RECENT)) + { + imapService.setFlag(fileInfo, Flags.Flag.RECENT, false); + } + } + return null; } - } - + }; + command.run(); } - - if (logger.isDebugEnabled() && folderInfo != null) - { - logger.debug(folderInfo.getName() + " - Recent count: " + count + " reset: " + reset); - } - return count; + return recent; } /** @@ -798,58 +599,21 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @return UIDNEXT value. */ @Override - protected long getUidNextInternal() + public long getUidNext() { - /** - * Can't used cached value since it may be out of date. - */ - return (Long) AuthenticationUtil.runAs(new GetUidValidityWork(folderInfo.getNodeRef(), serviceRegistry.getNodeService()), AuthenticationUtil.getSystemUserName()) + 1; + NavigableMap search = getFolderStatus().search; + return search.isEmpty() ? 1 : search.lastKey() + 1; } - public boolean isStale() - { - if(uidValidity == generateUidValidity()) - { - logger.debug("folder is not stale"); - return false; - } - else - { - logger.debug("folder is stale"); - return true; - } - } - /** * Returns UIDVALIDITY value of the folder. * * @return UIDVALIDITY value. */ @Override - protected long getUidValidityInternal() + public long getUidValidity() { - /** - * Can't used cached value uidValidity since it may be out of date. - */ - //return uidValidity; - return (Long) AuthenticationUtil.runAs(new GetUidValidityWork(folderInfo.getNodeRef(), serviceRegistry.getNodeService()), AuthenticationUtil.getSystemUserName()); - } - - /** - * Generates UIDVALIDITY value of the folder. - * - * @return UIDVALIDITY value. - */ - private long generateUidValidity() - { - if(folderInfo != null) - { - return (Long) AuthenticationUtil.runAs(new GenerateUidValidityWork(folderInfo.getNodeRef(), serviceRegistry.getNodeService()), AuthenticationUtil.getSystemUserName()); - } - else - { - return 0; - } + return getFolderStatus().uidValidity; } /** @@ -858,33 +622,9 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * @return Count of the unseen messages for current user. */ @Override - protected int getUnseenCountInternal() + public int getUnseenCount() { - if (logger.isDebugEnabled()) - { - logger.debug("[getUnseenCountInternal] " + this); - } - if (messages.size() == 0 && folderInfo != null) - { - List fileInfos = imapService.searchMails(folderInfo.getNodeRef(), viewMode); - convertToMessages(fileInfos); - } - - int count = 0; - Collection values = messages.values(); - for (SimpleStoredMessage message : values) - { - if (!getFlags(message).contains(Flags.Flag.SEEN)) - { - count++; - } - - } - if (logger.isDebugEnabled() && folderInfo != null) - { - logger.debug(folderInfo.getName() + " - Unseen count: " + count); - } - return count; + return getFolderStatus().unseenCount; } /** @@ -908,15 +648,12 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab throws FolderException, MessagingException { int msn = getMsn(uid); - SimpleStoredMessage message = messages.get(uid); - FileInfo fileInfo = ((AbstractMimeMessage) message.getMimeMessage()).getMessageInfo(); + FileInfo fileInfo = searchMails().get(uid); imapService.setFlags(fileInfo, MessageFlags.ALL_FLAGS, false); imapService.setFlags(fileInfo, flags, true); - message = new SimpleStoredMessage(message.getMimeMessage(), message.getInternalDate(), uid); - messages.put(uid, message); - + Long uidNotification = addUid ? uid : null; - notifyFlagUpdate(msn, message.getFlags(), uidNotification, silentListener); + notifyFlagUpdate(msn, flags, uidNotification, silentListener); } /** @@ -942,36 +679,18 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab throws MessagingException, FolderException { int msn = getMsn(uid); - SimpleStoredMessage message = (SimpleStoredMessage) messages.get(uid); - - imapService.setFlags(((AbstractMimeMessage) message.getMimeMessage()).getMessageInfo(), flags, value); - message = new SimpleStoredMessage(message.getMimeMessage(), message.getInternalDate(), uid); - messages.put(uid, message); - + FileInfo fileInfo = searchMails().get(uid); + imapService.setFlags(fileInfo, flags, value); + Long uidNotification = null; if (addUid) { uidNotification = new Long(uid); } - notifyFlagUpdate(msn, message.getFlags(), uidNotification, silentListener); + notifyFlagUpdate(msn, flags, uidNotification, silentListener); } - /** - * @param fileInfo - {@link FileInfo} representing message. - * @return UID of the message. - */ - private long getMessageUid(FileInfo fileInfo) - { - if (serviceRegistry.getNodeService().getType(fileInfo.getNodeRef()).equals(ContentModel.TYPE_FOLDER)) - { - long modifDate = ((Date) serviceRegistry.getNodeService().getProperty(fileInfo.getNodeRef(), ContentModel.PROP_MODIFIED)).getTime(); - return (modifDate - YEAR_2005)/1000; - } - - return (Long) serviceRegistry.getNodeService().getProperty(fileInfo.getNodeRef(), ContentModel.PROP_NODE_DBID); - } - private Flags getFlags(SimpleStoredMessage mess) { return ((AbstractMimeMessage) mess.getMimeMessage()).getFlags(); @@ -979,54 +698,24 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab // ----------------------Getters and Setters---------------------------- + public String getFolderPath() + { + return this.folderPath; + } + public FileInfo getFolderInfo() { return folderInfo; } - - public void setFolderName(String folderName) - { - this.folderName = folderName; - } - - public void setViewMode(ImapViewMode viewMode) - { - this.viewMode = viewMode; - } - - public void setMountPointName(String mountPointName) - { - this.mountPointName = mountPointName; - } - - public void setMountParent(NodeRef mountParent) - { - this.rootNodeRef = mountParent; - } - - /** - * Whether the folder is selectable. - * - * @return {@code boolean}. + + /* (non-Javadoc) + * @see org.alfresco.repo.imap.AbstractImapFolder#isMarkedInternal() */ @Override - protected boolean isSelectableInternal() + public boolean isMarked() { - - return this.selectable; - } - - /** - * Sets {@link #selectable} property. - * - * @param selectable - {@code boolean}. - */ - public void setSelectable(boolean selectable) - { - this.selectable = selectable; - // Map properties = folderInfo.getProperties(); - // properties.put(ImapModel.PROP_IMAP_FOLDER_SELECTABLE, this.selectable); - // imapHelper.setProperties(folderInfo, properties); + FolderStatus folderStatus = getFolderStatus(); + return folderStatus.recentCount > 0 || folderStatus.unseenCount > 0; } /** @@ -1052,15 +741,16 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab * * @param folderFileInfo The folder to create message in. * @param message The original MimeMessage. - * @return Wrapped AbstractMimeMessage which was created. + * @return ID of the new message created * @throws FileNotFoundException * @throws FileExistsException * @throws MessagingException * @throws IOException */ - private AbstractMimeMessage createMimeMessageInFolder( + private long createMimeMessageInFolder( FileInfo folderFileInfo, - MimeMessage message) + MimeMessage message, + Flags flags) throws FileExistsException, FileNotFoundException, IOException, MessagingException { String name = AlfrescoImapConst.MESSAGE_PREFIX + GUID.generate(); @@ -1069,133 +759,16 @@ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializab final long newMessageUid = (Long) messageFile.getProperties().get(ContentModel.PROP_NODE_DBID); name = AlfrescoImapConst.MESSAGE_PREFIX + newMessageUid + AlfrescoImapConst.EML_EXTENSION; fileFolderService.rename(messageFile.getNodeRef(), name); + Flags newFlags = new Flags(flags); + newFlags.add(Flag.RECENT); + imapService.setFlags(messageFile, newFlags, true); if (extractAttachmentsEnabled) { imapService.extractAttachments(folderFileInfo.getNodeRef(), messageFile.getNodeRef(), message); } - return new IncomingImapMessage(messageFile, serviceRegistry, message); - } - - private void removeMessageFromCache(long uid) - { - messages.remove(uid); - msnCache.remove(uid); - messagesCache.remove(uid); - } - - 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; - } - } - - /** - * Generate UID validity - * - * In general this class will return a long UID value but if there is no - * ASPECT_IMAP_FOLDER then running this method will add the aspect and add - * initial values. Needs to be run in a read/write transaction - */ - public class GenerateUidValidityWork implements RunAsWork - { - - private NodeService nodeService; - private NodeRef folderNodeRef; - - public GenerateUidValidityWork(NodeRef folderNodeRef, NodeService nodeService) - { - this.folderNodeRef = folderNodeRef; - this.nodeService = nodeService; - } - - @Override - public Long doWork() throws Exception - { - if(nodeService.exists(folderNodeRef)) - { - long modifDate = 0L; - if (nodeService.hasAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER)) - { - modifDate = ((Long) nodeService.getProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY)); - } - else - { - Date currDate = new Date(); - modifDate = currDate.getTime(); - Map aspectProperties = new HashMap(1, 1); - aspectProperties.put(ImapModel.PROP_UIDVALIDITY, modifDate); - nodeService.addAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER, aspectProperties); - } - return (modifDate - YEAR_2005) / 1000; - } - else - { - return new Long(0); - } - } - - } - - /** - * Read only transaction to get uidvalidity - * @author mrogers - */ - public class GetUidValidityWork implements RunAsWork - { - - private NodeService nodeService; - private NodeRef folderNodeRef; - - public GetUidValidityWork(NodeRef folderNodeRef, NodeService nodeService) - { - this.folderNodeRef = folderNodeRef; - this.nodeService = nodeService; - } - - @Override - public Long doWork() throws Exception - { - if(nodeService.exists(folderNodeRef)) - { - long modifDate = 0L; - if (nodeService.hasAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER)) - { - modifDate = ((Long) nodeService.getProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY)); - // we need tens part of the second at least, because - // we should avoid issues when several changes were completed within a second. - // so, divide by 100 instead of 1000. see ImapServiceImplCacheTest#testRepoBehaviourWithFoldersCache() - return (modifDate - YEAR_2005) / 100; - } - } - return new Long(0); - } - + // Force persistence of the message to the repository + new IncomingImapMessage(messageFile, serviceRegistry, message); + return newMessageUid; } } diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java b/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java index 7f19ad4430..859ad7b4f4 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -20,10 +20,13 @@ package org.alfresco.repo.imap; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.alfresco.repo.imap.exception.AlfrescoImapFolderException; -import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Utf7; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -34,14 +37,27 @@ import com.icegreen.greenmail.store.MailFolder; import com.icegreen.greenmail.user.GreenMailUser; /** + * This Host Manager is assumed to be session local, i.e. there is one instance per IMAP TCP connection. This means that + * it can locally cache items being interacted with during the session and its knowledge of which folders / messages do + * or do not exist will match that of the client. + * * @author Mike Shavnev + * @author David Ward */ public class AlfrescoImapHostManager implements ImapHostManager { private ImapService imapService; - private TransactionService transactionService; + private Map folderCache; private static Log logger = LogFactory.getLog(AlfrescoImapHostManager.class); + + /** + * @param imapService + */ + public AlfrescoImapHostManager(ImapService imapService) + { + this.imapService = imapService; + } /** * Returns the hierarchy delimiter for mailboxes on this host. @@ -67,13 +83,9 @@ public class AlfrescoImapHostManager implements ImapHostManager { try { - return new ArrayList( - imapService.listMailboxes( - new AlfrescoImapUser( - user.getEmail(), - user.getLogin(), - user.getPassword()), - mailboxPattern)); + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + return registerMailboxes(imapService.listMailboxes(alfrescoUser, getUnqualifiedMailboxPattern( + alfrescoUser, mailboxPattern), false)); } catch (Throwable e) { @@ -93,18 +105,15 @@ public class AlfrescoImapHostManager implements ImapHostManager * @return Collection of mailboxes matching the pattern. * @throws com.icegreen.greenmail.store.FolderException */ - public Collection listSubscribedMailboxes(GreenMailUser user, String mailboxPattern) throws FolderException + public Collection listSubscribedMailboxes(GreenMailUser user, String mailboxPattern) + throws FolderException { try - { - return new ArrayList( - imapService.listSubscribedMailboxes( - new AlfrescoImapUser( - user.getEmail(), - user.getLogin(), - user.getPassword()), - mailboxPattern)); - } + { + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + return registerMailboxes(imapService.listMailboxes(alfrescoUser, getUnqualifiedMailboxPattern( + alfrescoUser, mailboxPattern), true)); + } catch (Throwable e) { logger.debug(e.getMessage(), e); @@ -129,12 +138,21 @@ public class AlfrescoImapHostManager implements ImapHostManager */ public void renameMailbox(GreenMailUser user, String oldMailboxName, String newMailboxName) throws FolderException, AuthorizationException { - try - { - imapService.renameMailbox(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), oldMailboxName, newMailboxName); - } + try + { + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + String oldFolderPath = getUnqualifiedMailboxPattern(alfrescoUser, + oldMailboxName); + String newFolderpath = getUnqualifiedMailboxPattern(alfrescoUser, newMailboxName); + imapService.renameMailbox(alfrescoUser, oldFolderPath, newFolderpath); + if (folderCache != null) + { + folderCache.remove(oldFolderPath); + folderCache.remove(newFolderpath); + } + } catch (Throwable e) - { + { logger.debug(e.getMessage(), e); throw new FolderException(e.getMessage()); } @@ -154,11 +172,13 @@ public class AlfrescoImapHostManager implements ImapHostManager public MailFolder createMailbox(GreenMailUser user, String mailboxName) throws AuthorizationException, FolderException { try - { - return imapService.createMailbox(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), mailboxName); - } + { + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + return registerMailBox(imapService.getOrCreateMailbox(alfrescoUser, getUnqualifiedMailboxPattern( + alfrescoUser, mailboxName), false, true)); + } catch (Throwable e) - { + { logger.debug(e.getMessage(), e); throw new FolderException(e.getMessage()); } @@ -175,11 +195,17 @@ public class AlfrescoImapHostManager implements ImapHostManager public void deleteMailbox(GreenMailUser user, String mailboxName) throws FolderException, AuthorizationException { try - { - imapService.deleteMailbox(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), mailboxName); - } - catch (Throwable e) + { + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + String folderPath = getUnqualifiedMailboxPattern(alfrescoUser, mailboxName); + imapService.deleteMailbox(alfrescoUser, folderPath); + if (folderCache != null) { + folderCache.remove(folderPath); + } + } + catch (Throwable e) + { logger.debug(e.getMessage(), e); throw new FolderException(e.getMessage()); } @@ -199,23 +225,32 @@ public class AlfrescoImapHostManager implements ImapHostManager */ public MailFolder getFolder(GreenMailUser user, String mailboxName) { - return imapService.getFolder(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), mailboxName); - } + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + String folderPath = getUnqualifiedMailboxPattern( + alfrescoUser, mailboxName); + // Warm up the cache if necessary + if (folderCache == null) + { + registerMailboxes(imapService.listMailboxes(alfrescoUser, "*", true)); + } + // Try to retrieve from the cache + AlfrescoImapFolder result = folderCache.get(folderPath); + if (result != null && result.reset()) + { + return result; + } + + // Look up and cache as a last resort + return registerMailBox(imapService.getOrCreateMailbox(alfrescoUser, folderPath, true, false)); + } /** * Simply calls {@link #getFolder(GreenMailUser, String)}.

Added to implement {@link ImapHostManager}. */ - public MailFolder getFolder(final GreenMailUser user, final String mailboxName, boolean mustExist) throws FolderException - { - try - { - return getFolder(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), mailboxName); - } - catch (Throwable e) - { - logger.debug(e.getMessage(), e); - throw new FolderException(e.getMessage()); - } + public MailFolder getFolder(final GreenMailUser user, final String mailboxName, boolean mustExist) + throws FolderException + { + return getFolder(user, mailboxName); } /** @@ -226,15 +261,7 @@ public class AlfrescoImapHostManager implements ImapHostManager */ public MailFolder getInbox(GreenMailUser user) throws FolderException { - try - { - return getFolder(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), AlfrescoImapConst.INBOX_NAME); - } - catch (Throwable e) - { - logger.debug(e.getMessage(), e); - throw new FolderException(e.getMessage()); - } + return getFolder(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), AlfrescoImapConst.INBOX_NAME); } /** @@ -257,7 +284,8 @@ public class AlfrescoImapHostManager implements ImapHostManager { try { - imapService.subscribe(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), mailbox); + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + imapService.subscribe(alfrescoUser, getUnqualifiedMailboxPattern(alfrescoUser, mailbox)); } catch (Throwable e) { @@ -276,7 +304,8 @@ public class AlfrescoImapHostManager implements ImapHostManager { try { - imapService.unsubscribe(new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()), mailbox); + AlfrescoImapUser alfrescoUser = new AlfrescoImapUser(user.getEmail(), user.getLogin(), user.getPassword()); + imapService.unsubscribe(alfrescoUser, getUnqualifiedMailboxPattern(alfrescoUser, mailbox)); } catch (Throwable e) { @@ -291,28 +320,70 @@ public class AlfrescoImapHostManager implements ImapHostManager public List getAllMessages() { throw new UnsupportedOperationException(); + } + + private String getUnqualifiedMailboxPattern(AlfrescoImapUser user, String mailboxPattern) + { + mailboxPattern = Utf7.decode(mailboxPattern, Utf7.UTF7_MODIFIED); + if (mailboxPattern.startsWith(AlfrescoImapConst.NAMESPACE_PREFIX)) + { + int sepIndex = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + return sepIndex == -1 ? mailboxPattern.endsWith("*") ? "*" : "" : mailboxPattern.substring(sepIndex + 1); + } + return mailboxPattern; + } + + + /** + * Caches returned mail folder collection and converts to a generic MailFolder collection. + * + * @param mailboxes + * a Collection of Alfresco mailboxes + * @return a generic MailFolder collection associated with the session + */ + private Collection registerMailboxes(Collection mailboxes) + { + int size = mailboxes.size(); + if (size == 0) + { + return Collections.emptyList(); + } + Collection result = new ArrayList(size); + if (folderCache == null) + { + folderCache = new HashMap(size * 2); + } + for (AlfrescoImapFolder mailbox : mailboxes) + { + result.add(registerMailBox(mailbox)); + } + return result; } - // ----------------------Getters and Setters---------------------------- - - public ImapService getImapService() + /** + * Caches a returned mail folder and converts to a generic MailFolder. + * + * @param mailbox + * an Alfresco mailbox + * @return a generic MailFolder associated with the session + */ + private MailFolder registerMailBox(AlfrescoImapFolder mailbox) { - return imapService; + String folderPath = mailbox.getFolderPath(); + if ((mailbox.isSelectable() || folderPath.isEmpty()) && folderCache != null) + { + AlfrescoImapFolder oldFolder = folderCache.get(folderPath); + if (oldFolder != null + && oldFolder.getFolderInfo().getNodeRef().equals(mailbox.getFolderInfo().getNodeRef()) + && oldFolder.reset()) + { + mailbox = oldFolder; + } + else + { + folderCache.put(folderPath, mailbox); + } + } + return mailbox; } - - public void setImapService(ImapService imapService) - { - this.imapService = imapService; - } - - public TransactionService getTransactionService() - { - return transactionService; - } - - public void setTransactionService(TransactionService transactionService) - { - this.transactionService = transactionService; - } - } diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java b/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java index 193e486d6b..b2bea3a49e 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java @@ -18,14 +18,17 @@ */ package org.alfresco.repo.imap; -import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; import com.icegreen.greenmail.Managers; import com.icegreen.greenmail.imap.ImapHostManager; import com.icegreen.greenmail.imap.ImapServer; +import com.icegreen.greenmail.store.FolderException; import com.icegreen.greenmail.user.UserManager; import com.icegreen.greenmail.util.ServerSetup; @@ -41,9 +44,9 @@ public class AlfrescoImapServer extends AbstractLifecycleBean private int port = 143; private String host = "0.0.0.0"; - private ImapHostManager imapHostManager; private UserManager imapUserManager; + private ImapService imapService; private boolean imapServerEnabled; @@ -78,9 +81,9 @@ public class AlfrescoImapServer extends AbstractLifecycleBean return host; } - public void setImapHostManager(ImapHostManager imapHostManager) + public void setImapService(ImapService imapService) { - this.imapHostManager = imapHostManager; + this.imapService = imapService; } public void setImapUserManager(UserManager imapUserManager) @@ -113,11 +116,12 @@ public class AlfrescoImapServer extends AbstractLifecycleBean { if(serverImpl == null) { - Managers imapManagers = new Managers() + final Managers imapManagers = new Managers() { + // We create a new Host Manager instance per session to allow for session state tracking public ImapHostManager getImapHostManager() { - return imapHostManager; + return new AlfrescoImapHostManager(AlfrescoImapServer.this.imapService); } public UserManager getUserManager() @@ -128,6 +132,7 @@ public class AlfrescoImapServer extends AbstractLifecycleBean serverImpl = new ImapServer(new ServerSetup(port, host, ServerSetup.PROTOCOL_IMAP), imapManagers); serverImpl.startService(null); + if (logger.isInfoEnabled()) { logger.info("IMAP service started on host:port " + host + ":" + this.port + "."); diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java b/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java index 4d27467be2..3cf7e0eb2d 100644 --- a/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java @@ -39,7 +39,7 @@ public class AlfrescoImapUser implements GreenMailUser { this.email = email; this.userName = login; - this.password = password.toCharArray(); + this.password = password == null ? null : password.toCharArray(); } public void authenticate(String password) throws UserException diff --git a/source/java/org/alfresco/repo/imap/ContentModelMessage.java b/source/java/org/alfresco/repo/imap/ContentModelMessage.java index b23564a0d3..1247670a0c 100644 --- a/source/java/org/alfresco/repo/imap/ContentModelMessage.java +++ b/source/java/org/alfresco/repo/imap/ContentModelMessage.java @@ -35,20 +35,18 @@ import javax.mail.internet.MimeUtility; import org.alfresco.model.ContentModel; import org.alfresco.repo.imap.ImapService.EmailBodyFormat; -import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.extensions.surf.util.I18NUtil; public class ContentModelMessage extends AbstractMimeMessage { private Log logger = LogFactory.getLog(ContentModelMessage.class); - protected static final String DEFAULT_EMAIL_FROM = "alfresco@alfresco.org"; - protected static final String DEFAULT_EMAIL_TO = "alfresco@alfresco.org"; + protected static final String DEFAULT_EMAIL_FROM = "alfresco" + DEFAULT_SUFFIX; + protected static final String DEFAULT_EMAIL_TO = DEFAULT_EMAIL_FROM; public ContentModelMessage(FileInfo fileInfo, ServiceRegistry serviceRegistry, boolean generateBody) throws MessagingException { diff --git a/source/java/org/alfresco/repo/imap/ImapModelMessage.java b/source/java/org/alfresco/repo/imap/ImapModelMessage.java index 73484f6a0d..64f175658b 100644 --- a/source/java/org/alfresco/repo/imap/ImapModelMessage.java +++ b/source/java/org/alfresco/repo/imap/ImapModelMessage.java @@ -21,7 +21,9 @@ package org.alfresco.repo.imap; import java.io.IOException; import java.io.InputStream; +import javax.activation.DataHandler; import javax.mail.MessagingException; +import javax.mail.internet.MimePartDataSource; import javax.mail.util.SharedByteArrayInputStream; import org.alfresco.model.ContentModel; @@ -80,22 +82,31 @@ public class ImapModelMessage extends AbstractMimeMessage { ContentService contentService = serviceRegistry.getContentService(); ContentReader reader = contentService.getReader(messageFileInfo.getNodeRef(), ContentModel.PROP_CONTENT); + InputStream is = null; try { - InputStream is = reader.getContentInputStream(); + is = reader.getContentInputStream(); this.parse(is); - is.close(); - is = null; } catch (ContentIOException e) { //logger.error(e); throw new MessagingException("The error occured during message creation from content stream.", e); } - catch (IOException e) + finally { - //logger.error(e); - throw new MessagingException("The error occured during message creation from content stream.", e); + if (is != null) + { + try + { + is.close(); + } + catch (IOException e) + { + throw new MessagingException("The error occured during message creation from content stream.", e); + } + is = null; + } } } @@ -122,7 +133,7 @@ public class ImapModelMessage extends AbstractMimeMessage throw new MessagingException(e.getMessage(),e); } } - + /* protected void parse(InputStream inputstream) throws MessagingException { @@ -130,5 +141,4 @@ public class ImapModelMessage extends AbstractMimeMessage contentStream = inputstream; } */ - } diff --git a/source/java/org/alfresco/repo/imap/ImapService.java b/source/java/org/alfresco/repo/imap/ImapService.java index 49c5d84b91..4a666bc795 100644 --- a/source/java/org/alfresco/repo/imap/ImapService.java +++ b/source/java/org/alfresco/repo/imap/ImapService.java @@ -20,6 +20,7 @@ package org.alfresco.repo.imap; import java.io.IOException; import java.util.List; +import java.util.NavigableMap; import javax.mail.Flags; import javax.mail.MessagingException; @@ -30,6 +31,8 @@ import org.alfresco.repo.imap.AlfrescoImapConst.ImapViewMode; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.NodeRef; +import com.icegreen.greenmail.store.SimpleStoredMessage; + /** * @author Arseny Kovalchuk * @since 3.2 @@ -85,36 +88,18 @@ public interface ImapService * Returns an collection of mailboxes. This method serves LIST command of the IMAP protocol. * * @param user User making the request - * @param mailboxPattern String name of a mailbox encoded in MUTF-7, possible including a wildcard. + * @param mailboxPattern String name of a mailbox, possible including a wildcard. + * @param listSubscribed list only subscribed folders? * @return Collection of mailboxes matching the pattern. */ - public List listMailboxes(AlfrescoImapUser user, String mailboxPattern); - - /** - * Returns an collection of subscribed mailboxes. This method serves LSUB command of the IMAP protocol. - * - * @param user User making the request - * @param mailboxPattern String name of a mailbox encoded in MUTF-7, possible including a wildcard. - * @return Collection of mailboxes matching the pattern. - */ - public List listSubscribedMailboxes(AlfrescoImapUser user, String mailboxPattern); - - /** - * Returns a reference to a newly created mailbox. The request should specify a mailbox that does not already exist on this server, that could exist on this server and that the - * user has rights to create. This method serves CREATE command of the IMAP protocol. - * - * @param user User making the request. - * @param mailboxName String name of the target encoded in MUTF-7, - * @return an Mailbox reference. - */ - public AlfrescoImapFolder createMailbox(AlfrescoImapUser user, String mailboxName); + public List listMailboxes(AlfrescoImapUser user, String mailboxPattern, boolean listSubscribed); /** * Deletes an existing MailBox. Specified mailbox must already exist on this server, and the user must have rights to delete it. This method serves DELETE command of the IMAP * protocol. * * @param user User making the request. - * @param mailboxName String name of the target encoded in MUTF-7, + * @param mailboxName String name of the target, * @throws com.icegreen.greenmail.store.FolderException if mailbox has a non-selectable store with children */ public void deleteMailbox(AlfrescoImapUser user, String mailboxName); @@ -126,35 +111,40 @@ public interface ImapService * protocol. * * @param user User making the request. - * @param oldMailboxName String name of the existing folder encoded in MUTF-7, - * @param newMailboxName String target new name encoded in MUTF-7, + * @param oldMailboxName String name of the existing folder + * @param newMailboxName String target new name */ public void renameMailbox(AlfrescoImapUser user, String oldMailboxName, String newMailboxName); /** - * Returns a reference to an existing Mailbox. The requested mailbox must already exists on this server and the requesting user must have at least lookup rights.

It is - * also can be used by to obtain hierarchy delimiter by the LIST command:

C: 2 list "" ""

S: * LIST () "." ""

S: 2 OK LIST completed. + * Returns a reference to a mailbox, either creating a new one or retrieving an existing one. * - * @param user User making the request. - * @param mailboxName String name of the target encoded in MUTF-7,. - * @return an Mailbox reference. + * @param user + * User making the request. + * @param mailboxName + * String name of the target. + * @param mayExist + * Is the mailbox allowed to exist already? If false and the mailbox already exists, an error will be thrown + * @param mayCreate + * If the mailbox does not exist, can one be created? If false then an error is thrown if the folder does not exist + * @return a Mailbox reference */ - public AlfrescoImapFolder getFolder(AlfrescoImapUser user, String mailboxName); + public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist, boolean mayCreate); /** - * Get root reference for the specified mailbox + * Get the node ref of the user's imap home. Will create it on demand if it + * does not already exist. * - * @param mailboxName mailbox name in IMAP client. - * @param userName - * @return NodeRef of root reference for the specified mailbox + * @param userName user name + * @return user IMAP home reference and create it if it doesn't exist. */ - public NodeRef getMailboxRootRef(String mailboxName, String userName); + public NodeRef getUserImapHomeRef(final String userName); /** * Subscribes a user to a mailbox. The mailbox must exist locally and the user must have rights to modify it.

This method serves SUBSCRIBE command of the IMAP protocol. * * @param user User making the request - * @param mailbox String representation of a mailbox name encoded in MUTF-7,. + * @param mailbox String representation of a mailbox name. */ public void subscribe(AlfrescoImapUser user, String mailbox); @@ -162,7 +152,7 @@ public interface ImapService * Unsubscribes from a given mailbox.

This method serves UNSUBSCRIBE command of the IMAP protocol. * * @param user User making the request - * @param mailbox String representation of a mailbox name encoded in MUTF-7,. + * @param mailbox String representation of a mailbox name. */ public void unsubscribe(AlfrescoImapUser user, String mailbox); @@ -196,7 +186,30 @@ public interface ImapService * @param includeSubFolders includeSubFolders * @return list of emails that context folder contains. */ - public List searchMails(NodeRef contextNodeRef, ImapViewMode viewMode); + public FolderStatus getFolderStatus(final String userName, final NodeRef contextNodeRef, ImapViewMode viewMode); + + /** + * Gets a cached MIME message for the given file, complete with message body. + * + * @param messageFileInfo imap file info. + * @return a message. + */ + public SimpleStoredMessage getMessage(FileInfo messageFileInfo) throws MessagingException; + + /** + * Creates a MIME message for the given file + * + * @param messageFileInfo imap file info. + * @param generateBody Should the message body be generated? + * @return a message. + */ + public SimpleStoredMessage createImapMessage(FileInfo messageFileInfo, boolean generateBody) throws MessagingException; + + /** + * Expunges (deletes) an IMAP message if its flags indicates + * @param messageFileInfo imap file info. + */ + public void expungeMessage(FileInfo messageFileInfo); /** * Return flags that belong to the specified imap folder. @@ -291,4 +304,27 @@ public interface ImapService * @return true if enabled */ public boolean getImapServerEnabled(); + + static class FolderStatus + { + public final int messageCount; + public final int recentCount; + public final int firstUnseen; + public final int unseenCount; + public final long uidValidity; + public final String changeToken; + public final NavigableMap search; + + public FolderStatus(int messageCount, int recentCount, int firstUnseen, int unseenCount, long uidValidity, + String changeToken, NavigableMap search) + { + this.messageCount = messageCount; + this.recentCount = recentCount; + this.firstUnseen = firstUnseen; + this.unseenCount = unseenCount; + this.uidValidity = uidValidity; + this.changeToken = changeToken; + this.search = search; + } + } } diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java index 8290567677..2cf79f3e6a 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImpl.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2010 Alfresco Software Limited. + * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * @@ -28,7 +28,9 @@ 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; @@ -36,9 +38,13 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.mail.Flags; +import javax.mail.MessagingException; import javax.mail.Flags.Flag; import javax.mail.MessagingException; import javax.mail.Multipart; @@ -57,6 +63,7 @@ import org.alfresco.repo.imap.config.ImapConfigMountPointsBean; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy; import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; @@ -67,13 +74,15 @@ import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.site.SiteModel; import org.alfresco.repo.site.SiteServiceException; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; -import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.lock.NodeLockedException; +import org.alfresco.service.cmr.model.FileExistsException; import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileFolderUtil; import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.model.SubFolderFilter; import org.alfresco.service.cmr.preference.PreferenceService; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -90,8 +99,10 @@ import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.site.SiteInfo; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.alfresco.util.MaxSizeMap; +import org.alfresco.util.Pair; import org.alfresco.util.PropertyCheck; -import org.alfresco.util.Utf7; import org.alfresco.util.config.RepositoryFolderConfigBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -101,18 +112,18 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean; import org.springframework.extensions.surf.util.I18NUtil; import org.springframework.util.FileCopyUtils; -import com.icegreen.greenmail.imap.ImapConstants; +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 +public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPolicy, OnDeleteChildAssociationPolicy, OnUpdatePropertiesPolicy, BeforeDeleteNodePolicy { private Log logger = LogFactory.getLog(ImapServiceImpl.class); - private static final String ERROR_PERMISSION_DENIED = "imap.server.error.permission_denied"; private static final String ERROR_FOLDER_ALREADY_EXISTS = "imap.server.error.folder_already_exist"; private static final String ERROR_MAILBOX_NAME_IS_MANDATORY = "imap.server.error.mailbox_name_is_mandatory"; private static final String ERROR_CANNOT_GET_A_FOLDER = "imap.server.error.cannot_get_a_folder"; @@ -120,7 +131,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol private static final String CHECKED_NODES = "imap.flaggable.aspect.checked.list"; private static final String FAVORITE_SITES = "imap.favorite.sites.list"; - private static final String UIDVALIDITY_LISTENER_ALREADY_BOUND = "imap.uidvalidity.already.bound"; + private static final String UIDVALIDITY_TRANSACTION_LISTENER = "imap.uidvalidity.txn.listener"; private SysAdminParams sysAdminParams; private FileFolderService fileFolderService; @@ -130,16 +141,16 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol private BehaviourFilter policyBehaviourFilter; private MimetypeService mimetypeService; - /** - * Folders cache - * - * Key : folder name, Object : AlfrescoImapFolder - */ - private SimpleCache foldersCache; - + // Note that this cache need not be cluster synchronized, as it is keyed by the cluster-safe change token + private Map, FolderStatus> folderCache; + private int folderCacheSize = 1000; + private ReentrantReadWriteLock folderCacheLock = new ReentrantReadWriteLock(); + private SimpleCache messageCache; private Map imapConfigMountPoints; private RepositoryFolderConfigBean[] ignoreExtractionFoldersBeans; private RepositoryFolderConfigBean imapHomeConfigBean; + + private NodeRef imapHomeNodeRef; private Set ignoreExtractionFolders; @@ -193,10 +204,18 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol @Override protected void onBootstrap(ApplicationEvent event) { - if (service.getImapServerEnabled()) + service.serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() { - service.startup(); - } + @Override + public Void execute() throws Throwable + { + if (service.getImapServerEnabled()) + { + service.startup(); + } + return null; + } + }); } @Override @@ -213,20 +232,10 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol { this.sysAdminParams = sysAdminParams; } - - public void setFoldersCache(SimpleCache foldersCache) + + public void setMessageCache(SimpleCache messageCache) { - this.foldersCache = foldersCache; - } - - public SimpleCache getFoldersCache() - { - return foldersCache; - } - - public FileFolderService getFileFolderService() - { - return fileFolderService; + this.messageCache = messageCache; } public void setFileFolderService(FileFolderService fileFolderService) @@ -263,6 +272,11 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol { this.imapHomeConfigBean = imapHomeConfigBean; } + + public void setFolderCacheSize(int folderCacheSize) + { + this.folderCacheSize = folderCacheSize; + } public String getDefaultFromAddress() { @@ -350,6 +364,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol PropertyCheck.mandatory(this, "repositoryTemplatePath", repositoryTemplatePath); PropertyCheck.mandatory(this, "policyBehaviourFilter", policyBehaviourFilter); PropertyCheck.mandatory(this, "mimetypeService", mimetypeService); + this.folderCache = new MaxSizeMap, FolderStatus>(folderCacheSize, false); // be sure that a default e-mail is correct try @@ -377,21 +392,11 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol public void startup() { - bindBeahaviour(); + bindBehaviour(); final NamespaceService namespaceService = serviceRegistry.getNamespaceService(); final SearchService searchService = serviceRegistry.getSearchService(); - - // Hit the mount points for early failure - AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() - { - public Void doWork() throws Exception - { - getMountPoints(); - return null; - } - }, AuthenticationUtil.getSystemUserName()); - + // Get NodeRefs for folders to ignore this.ignoreExtractionFolders = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() { @@ -425,138 +430,183 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol return imapHomeConfigBean.getOrCreateFolderPath(namespaceService, nodeService, searchService, fileFolderService); } }, AuthenticationUtil.getSystemUserName()); + + // Hit the mount points and warm the caches for early failure + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Void doWork() throws Exception + { + for (String mountPointName: imapConfigMountPoints.keySet()) + { + for (AlfrescoImapFolder mailbox : listMailboxes(new AlfrescoImapUser(null, AuthenticationUtil + .getSystemUserName(), null), mountPointName + "*", false)) + { + mailbox.getUidNext(); + } + } + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } public void shutdown() { } - protected void bindBeahaviour() + 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, - ContentModel.TYPE_FOLDER, + ImapModel.ASPECT_IMAP_FOLDER, ContentModel.ASSOC_CONTAINS, - new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); - policyComponent.bindAssociationBehaviour( - OnCreateChildAssociationPolicy.QNAME, - ContentModel.TYPE_CONTENT, - ContentModel.ASSOC_CONTAINS, - new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); + new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT)); policyComponent.bindAssociationBehaviour( OnDeleteChildAssociationPolicy.QNAME, - ContentModel.TYPE_FOLDER, + ImapModel.ASPECT_IMAP_FOLDER, ContentModel.ASSOC_CONTAINS, - new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); - policyComponent.bindAssociationBehaviour( - OnDeleteChildAssociationPolicy.QNAME, + new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.EVERY_EVENT)); + policyComponent.bindClassBehaviour( + OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_CONTENT, - ContentModel.ASSOC_CONTAINS, - new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); + new JavaBehaviour(this, "onUpdateProperties", NotificationFrequency.EVERY_EVENT)); policyComponent.bindClassBehaviour( BeforeDeleteNodePolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); - policyComponent.bindClassBehaviour( - BeforeDeleteNodePolicy.QNAME, - ContentModel.TYPE_FOLDER, - new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); } // ---------------------- Service Methods -------------------------------- - public List listSubscribedMailboxes(AlfrescoImapUser user, String mailboxPattern) + public SimpleStoredMessage getMessage(FileInfo mesInfo) throws MessagingException { - mailboxPattern = Utf7.decode(mailboxPattern, Utf7.UTF7_MODIFIED); - - if (logger.isDebugEnabled()) + NodeRef nodeRef = mesInfo.getNodeRef(); + Date modified = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED); + if(modified != null) { - logger.debug("Listing subscribed mailboxes: mailboxPattern=" + mailboxPattern); + 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; } - mailboxPattern = getMailPathInRepo(mailboxPattern); - if (logger.isDebugEnabled()) + else { - logger.debug("Listing subscribed mailboxes: mailbox path in Alfresco=" + mailboxPattern); + 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); } - return listMailboxes(user, mailboxPattern, true); } - public List listMailboxes(AlfrescoImapUser user, String mailboxPattern) + public void expungeMessage(FileInfo fileInfo) { - mailboxPattern = Utf7.decode(mailboxPattern, Utf7.UTF7_MODIFIED); - - if (logger.isDebugEnabled()) + Flags flags = getFlags(fileInfo); + if (flags.contains(Flags.Flag.DELETED)) { - logger.debug("Listing mailboxes: mailboxPattern=" + mailboxPattern); + fileFolderService.delete(fileInfo.getNodeRef()); + messageCache.remove(fileInfo.getNodeRef()); } - mailboxPattern = getMailPathInRepo(mailboxPattern); - if (logger.isDebugEnabled()) - { - logger.debug("Listing mailboxes: mailbox path in Alfresco Repository = " + mailboxPattern); - } - - return listMailboxes(user, mailboxPattern, false); } - - public AlfrescoImapFolder createMailbox(AlfrescoImapUser user, String mailboxName) + + public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist, boolean mayCreate) { if (mailboxName == null) { throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY)); } - mailboxName = Utf7.decode(mailboxName, Utf7.UTF7_MODIFIED); - if (logger.isDebugEnabled()) + // A request for the hierarchy delimiter + if (mailboxName.length() == 0) { - logger.debug("Creating mailbox: " + mailboxName); + return new AlfrescoImapFolder(user.getLogin(), serviceRegistry); } - NodeRef root = getMailboxRootRef(mailboxName, user.getLogin()); - NodeRef parentNodeRef = root; // it is used for hierarhy deep search. - for (String folderName : getMailPathInRepo(mailboxName).split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER))) + final NodeRef root; + final List pathElements; + ImapViewMode viewMode = ImapViewMode.ARCHIVE; + int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (index < 0) { - NodeRef child = fileFolderService.searchSimple(parentNodeRef, folderName); - - if (logger.isDebugEnabled()) + root = getUserImapHomeRef(user.getLogin()); + pathElements = Collections.singletonList(mailboxName); + } + else + { + String rootPath = mailboxName.substring(0, index); + ImapConfigMountPointsBean imapConfigMountPoint = this.imapConfigMountPoints.get(rootPath); + if (imapConfigMountPoint != null) { - logger.debug("Trying to create folder '" + folderName + "'"); - } - if (child == null) - { - // folder doesn't exist - AccessStatus status = permissionService.hasPermission(parentNodeRef, PermissionService.CREATE_CHILDREN); - if (status == AccessStatus.DENIED) - { - throw new AlfrescoRuntimeException(ERROR_PERMISSION_DENIED); - } - FileInfo mailFolder = serviceRegistry.getFileFolderService().create(parentNodeRef, folderName, ContentModel.TYPE_FOLDER); - AlfrescoImapFolder resultFolder = new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - mailFolder, - folderName, - getViewMode(mailboxName), - root, - getMountPointName(mailboxName), - isExtractionEnabled(mailFolder.getNodeRef()), - serviceRegistry); - foldersCache.put(mailboxName, resultFolder); - return resultFolder; + root = imapConfigMountPoint.getFolderPath(serviceRegistry.getNamespaceService(), nodeService, serviceRegistry.getSearchService(), fileFolderService); + pathElements = Arrays.asList(mailboxName.substring(index + 1).split( + String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER))); + viewMode = imapConfigMountPoint.getMode(); } else - { - // folder already exists - if (logger.isDebugEnabled()) - { - logger.debug("Folder '" + folderName + "' already exists"); - } - // next search from new parent - parentNodeRef = child; + { + root = getUserImapHomeRef(user.getLogin()); + pathElements = Arrays.asList(mailboxName.split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER))); } } - throw new AlfrescoRuntimeException(ERROR_FOLDER_ALREADY_EXISTS); + 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())); } public void deleteMailbox(AlfrescoImapUser user, String mailboxName) @@ -570,7 +620,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY)); } - AlfrescoImapFolder folder = getFolder(user, mailboxName); + AlfrescoImapFolder folder = getOrCreateMailbox(user, mailboxName, true, false); NodeRef nodeRef = folder.getFolderInfo().getNodeRef(); List childFolders = fileFolderService.listFolders(nodeRef); @@ -599,7 +649,6 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol throw new AlfrescoRuntimeException(mailboxName + " - Can't delete a non-selectable store with children."); } } - foldersCache.remove(mailboxName); } public void renameMailbox(AlfrescoImapUser user, String oldMailboxName, String newMailboxName) @@ -609,467 +658,50 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol throw new IllegalArgumentException(ERROR_MAILBOX_NAME_IS_MANDATORY); } - AlfrescoImapFolder sourceNode = getFolder(user, oldMailboxName); + AlfrescoImapFolder sourceNode = getOrCreateMailbox(user, oldMailboxName, true, false); - oldMailboxName = Utf7.decode(oldMailboxName, Utf7.UTF7_MODIFIED); - newMailboxName = Utf7.decode(newMailboxName, Utf7.UTF7_MODIFIED); if (logger.isDebugEnabled()) { logger.debug("Renaming folder oldMailboxName=" + oldMailboxName + " newMailboxName=" + newMailboxName); } - - NodeRef root = getMailboxRootRef(oldMailboxName, user.getLogin()); - String[] folderNames = getMailPathInRepo(newMailboxName).split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); - String folderName = null; - NodeRef parentNodeRef = root; // initial root for search - try - { - for (int i = 0; i < folderNames.length; i++) - { - folderName = folderNames[i]; - if (i == (folderNames.length - 1)) // is it the last element - { - FileInfo newFileInfo = null; - if (oldMailboxName.equalsIgnoreCase(AlfrescoImapConst.INBOX_NAME)) - { - // If you trying to rename INBOX - // - just copy it to another folder with new name - // and leave INBOX (with children) intact. - newFileInfo = fileFolderService.copy(sourceNode.getFolderInfo().getNodeRef(), parentNodeRef, folderName); - } - else - { - newFileInfo = fileFolderService.move( - sourceNode.getFolderInfo().getNodeRef(), - parentNodeRef, folderName); - } - - foldersCache.remove(oldMailboxName); - AlfrescoImapFolder resultFolder = new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - newFileInfo, - folderName, - getViewMode(newMailboxName), - root, - getMountPointName(newMailboxName), - isExtractionEnabled(newFileInfo.getNodeRef()), - serviceRegistry); - foldersCache.put(newMailboxName, resultFolder); - } - else - { - // not last element than checks if it exists and creates if doesn't - NodeRef child = fileFolderService.searchSimple(parentNodeRef, folderName); - - if (child == null) - { - // check creation permission - AccessStatus status = permissionService.hasPermission(parentNodeRef, PermissionService.CREATE_CHILDREN); - if (status == AccessStatus.DENIED) - { - throw new AlfrescoRuntimeException(ERROR_PERMISSION_DENIED); - } - - if (logger.isDebugEnabled()) - { - logger.debug("Creating folder '" + folderName + "'"); - } - serviceRegistry.getFileFolderService().create(parentNodeRef, folderName, ContentModel.TYPE_FOLDER); - } - else - { - parentNodeRef = child; - if (logger.isDebugEnabled()) - { - logger.debug("Folder '" + folderName + "' already exists"); - } - } - } - } - } - catch (Exception e) - { - if (e instanceof AlfrescoRuntimeException) - { - throw (AlfrescoRuntimeException) e; - } - else - { - throw new AlfrescoRuntimeException(e.getMessage(), e); - } - } - } - - public AlfrescoImapFolder getFolder(AlfrescoImapUser user, String mailboxName) - { - if (logger.isDebugEnabled()) - { - logger.debug("Folders cache size is " + foldersCache.getKeys().size()); - } - mailboxName = Utf7.decode(mailboxName, Utf7.UTF7_MODIFIED); - if (logger.isDebugEnabled()) - { - logger.debug("Get folder '" + mailboxName + "'"); - } - // If MailFolder object is used to obtain hierarchy delimiter by LIST command: - // Example: - // C: 2 list "" "" - // S: * LIST () "." "" - // S: 2 OK LIST completed. - if ("".equals(mailboxName)) - { - if (logger.isDebugEnabled()) - { - logger.debug("Request for the hierarchy delimiter"); - } - AlfrescoImapFolder hierarhyFolder = (AlfrescoImapFolder) foldersCache.get("hierarhy.delimeter"); - if (logger.isDebugEnabled()) - { - logger.debug("Got a hierarhy delimeter from cache: " + hierarhyFolder); - } - if (hierarhyFolder == null) - { - hierarhyFolder = new AlfrescoImapFolder(user.getQualifiedMailboxName(), serviceRegistry); - // TEMP Comment out putting this into the same cache as the "real folders" Causes NPE in - // Security Interceptor - //foldersCache.put("hierarhy.delimeter", hierarhyFolder); - } - return hierarhyFolder; - } - else if (AlfrescoImapConst.INBOX_NAME.equalsIgnoreCase(mailboxName) || AlfrescoImapConst.TRASH_NAME.equalsIgnoreCase(mailboxName)) - { - String cacheKey = user.getLogin() + '.' + mailboxName; - AlfrescoImapFolder imapSystemFolder = (AlfrescoImapFolder) foldersCache.get(cacheKey); - - if(imapSystemFolder != null) - { - if (logger.isDebugEnabled()) - { - logger.debug("Got a system folder '" + mailboxName + "' from cache: " + imapSystemFolder); - } - - /** - * Check whether resultFolder is stale - */ - if(imapSystemFolder.isStale()) - { - logger.debug("system folder is stale"); - imapSystemFolder = null; - } - } - - if (imapSystemFolder == null) - { - NodeRef userImapRoot = getUserImapHomeRef(user.getLogin()); - NodeRef mailBoxRef = nodeService.getChildByName(userImapRoot, ContentModel.ASSOC_CONTAINS, mailboxName); - if (mailBoxRef != null) - { - FileInfo mailBoxFileInfo = fileFolderService.getFileInfo(mailBoxRef); - imapSystemFolder = new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - mailBoxFileInfo, - mailBoxFileInfo.getName(), - getViewMode(mailboxName), - userImapRoot, - getMountPointName(mailboxName), - isExtractionEnabled(mailBoxFileInfo.getNodeRef()), - serviceRegistry); - foldersCache.put(cacheKey, imapSystemFolder); - if (logger.isDebugEnabled()) - { - logger.debug("Returning folder '" + mailboxName + "'"); - } - } - else - { - if (logger.isDebugEnabled()) - { - logger.debug("Cannot get a folder '" + mailboxName + "'"); - } - throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] { mailboxName }); - } - } - return imapSystemFolder; - - } - - /** - * Folder is not hierarchy.delimiter or a "System" folder (INBOX or TRASH) - */ - AlfrescoImapFolder resultFolder = (AlfrescoImapFolder) foldersCache.get(mailboxName); - if(resultFolder != null) - { - if (logger.isDebugEnabled()) - { - logger.debug("Got a folder '" + mailboxName + "' from cache: " + resultFolder); - } - - /** - * Check whether resultFolder is stale - */ - /* - if(resultFolder.isStale()) - { - logger.debug("folder is stale"); - resultFolder = null; - } - */ - } - - if (resultFolder == null) + NodeRef newMailParent; + String newMailName; + int index = newMailboxName.lastIndexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (index < 0) { - ImapViewMode viewMode = getViewMode(mailboxName); - String mountPointName = getMountPointName(mailboxName); - - NodeRef root = getMailboxRootRef(mailboxName, user.getLogin()); - NodeRef nodeRef = root; // initial top folder - - String[] folderNames = getMailPathInRepo(mailboxName).split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); - - if(folderNames.length == 1 && folderNames[0].length() == 0) - { - // This is the root of the mount point e.g "Alfresco IMAP" which has a path from root of "" - FileInfo folderFileInfo = fileFolderService.getFileInfo(root); - - resultFolder = new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - folderFileInfo, - mountPointName, - viewMode, - root, - mountPointName, - isExtractionEnabled(folderFileInfo.getNodeRef()), - serviceRegistry); - - if (logger.isDebugEnabled()) - { - logger.debug("Returning root folder '" + mailboxName + "'"); - } - } - else - { - - for (int i = 0; i < folderNames.length; i++) - { - if (logger.isDebugEnabled()) - { - logger.debug("Processing of '" + folderNames[i] + "'"); - } - - NodeRef targetNode = fileFolderService.searchSimple(nodeRef, folderNames[i]); - - if (i == 0 && targetNode == null) - { - resultFolder = new AlfrescoImapFolder(user.getQualifiedMailboxName(), serviceRegistry); - if (logger.isDebugEnabled()) - { - logger.debug("Returning empty folder '" + folderNames[i] + "'"); - } - // skip cache for root - return resultFolder; - } - - if (i == (folderNames.length - 1)) // is last - { - FileInfo folderFileInfo = fileFolderService.getFileInfo(targetNode); - - if (logger.isDebugEnabled()) - { - logger.debug("Found folder to list: " + folderFileInfo.getName()); - } - - resultFolder = new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - folderFileInfo, - folderFileInfo.getName(), - viewMode, - root, - mountPointName, - isExtractionEnabled(folderFileInfo.getNodeRef()), - serviceRegistry); - - if (logger.isDebugEnabled()) - { - logger.debug("Returning folder '" + mailboxName + "'"); - } - } - - /** - * End of loop - next element in path - */ - nodeRef = targetNode; - } - } - - if (resultFolder == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("Cannot get a folder '" + mailboxName + "'"); - } - throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[] { mailboxName }); - } - else - { - foldersCache.put(mailboxName, resultFolder); - if (logger.isDebugEnabled()) - { - logger.debug("Put a folder '" + mailboxName + "' to cache: " + resultFolder); - } - } - } - return resultFolder; - - } - - /** - * Deep search for mailboxes/folders in the specified context - * - * Certain folders are excluded depending upon the view mode. - * - For ARCHIVE mode all Share Sites are excluded. - * - For MIXED and VIRTUAL non favourite sites are excluded. - * - * @param contextNodeRef context folder for search - * @param viewMode is folder in "Virtual" View - * @return list of mailboxes/folders - */ - private List searchDeep(final NodeRef contextNodeRef, final ImapViewMode viewMode) - { - if (logger.isDebugEnabled()) - { - logger.debug("[searchDeep] Start. nodeRef=" + contextNodeRef + ", viewMode=" + viewMode); - } - - List searchResult = fileFolderService.listDeepFolders(contextNodeRef, new ImapSubFolderFilter(viewMode)); - - if (logger.isDebugEnabled()) - { - logger.debug("[searchDeep] End"); - } - return new ArrayList(searchResult); - } - - - /** - * Shallow search for mailboxes in specified context - * - * @param contextNodeRef context folder for search - * @param viewMode is folder in "Virtual" View - * @param namePattern name pattern for search - * @return list of mailboxes - */ - private List searchByPattern(final NodeRef contextNodeRef, final ImapViewMode viewMode, final String namePattern) - { - if (logger.isDebugEnabled()) - { - logger.debug("[searchByPattern] Start. nodeRef=" + contextNodeRef + ", viewMode=" + viewMode + " namePattern=" + namePattern); - } - - List searchResult; - - /** - * Shallow search for all folders below contextNodeRef - */ - if("*".equals(namePattern)) - { - /** - * This is a simple listing of all folders below contextNodeRef - */ - logger.debug("call file folder service to list folders"); - - searchResult = fileFolderService.listFolders(contextNodeRef); + newMailParent = getUserImapHomeRef(user.getLogin()); + newMailName = newMailboxName; } else { - logger.debug("call listDeepFolders"); - searchResult = fileFolderService.listDeepFolders(contextNodeRef, new ImapSubFolderFilter(viewMode, namePattern)); + newMailParent = getOrCreateMailbox(user, newMailboxName.substring(0, index), true, true).getFolderInfo().getNodeRef(); + newMailName = newMailboxName.substring(index + 1); } - - -// DO WE NEED TO WORRY ABOUT THE STUFF BELOW ? -// WOULD ONLY BE RELEVANT if ContextNodeRef is site root. -// IF contextNodeRef is the imap home or the contextNodeRef is in the depths of a non favourite site. -// -// Set result = new HashSet(searchResult); -// if (viewMode == ImapViewMode.VIRTUAL || viewMode == ImapViewMode.MIXED) -// { -// /** -// * In VIRTUAL and MIXED MODE WE SHOULD ONLY DISPLAY FAVOURITE SITES -// */ -// List nonFavSites = getNonFavouriteSites(getCurrentUser()); -// for (SiteInfo siteInfo : nonFavSites) -// { -// FileInfo nonFavSite = fileFolderService.getFileInfo(siteInfo.getNodeRef()); -// -// // search deep for all folders in the site -// List siteChilds = fileFolderService.search(nonFavSite.getNodeRef(), namePattern, false, true, true); -// result.removeAll(siteChilds); -// result.remove(nonFavSite); -// } -// -// } -// else -// { -// /** -// * IN ARCHIVE MODE we don't display folders any SITES -// */ -// // Remove folders from Sites -// List sites = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback>() -// { -// public List execute() throws Exception -// { -// List res = new ArrayList(); -// try -// { -// -// res = serviceRegistry.getSiteService().listSites(getCurrentUser()); -// } -// catch (SiteServiceException e) -// { -// // Do nothing. Root sites folder was not created. -// if (logger.isWarnEnabled()) -// { -// logger.warn("Root sites folder was not created."); -// } -// } -// catch (InvalidNodeRefException e) -// { -// // Do nothing. Root sites folder was deleted. -// if (logger.isWarnEnabled()) -// { -// logger.warn("Root sites folder was deleted."); -// } -// } -// -// if (logger.isDebugEnabled()) -// { -// logger.debug("Search folders return "); -// } -// -// return res; -// } -// }, false, true); -// -// for (SiteInfo siteInfo : sites) -// { -// List siteChilds = fileFolderService.search(siteInfo.getNodeRef(), namePattern, false, true, true); -// result.removeAll(siteChilds); -// // remove site -// result.remove(fileFolderService.getFileInfo(siteInfo.getNodeRef())); -// } -// -// } - - if (logger.isDebugEnabled()) + + try { - if (logger.isDebugEnabled()) + if (oldMailboxName.equalsIgnoreCase(AlfrescoImapConst.INBOX_NAME)) { - logger.debug("[searchByPattern] End. nodeRef=" + contextNodeRef + ", viewMode=" + viewMode + ", namePattern=" + namePattern + ", searchResult=" +searchResult.size()); + // 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); } } - - return searchResult; + catch (FileNotFoundException e) + { + throw new AlfrescoRuntimeException(e.getMessage(), e); + } + catch (FileExistsException e) + { + throw new AlfrescoRuntimeException(e.getMessage(), e); + } } /** @@ -1081,44 +713,129 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol * @param viewMode context folder view mode * @return list of emails that context folder contains. */ - public List searchMails(NodeRef contextNodeRef, ImapViewMode viewMode) + public FolderStatus getFolderStatus(final String userName, final NodeRef contextNodeRef, ImapViewMode viewMode) { if (logger.isDebugEnabled()) { - logger.debug("Search mails contextNodeRef=" + contextNodeRef + ", viewMode=" + viewMode ); + logger.debug("Search mails contextNodeRef=" + contextNodeRef + ", viewMode=" + viewMode); } - - List searchResult = fileFolderService.listFiles(contextNodeRef); - - List result = new LinkedList(); - //List searchResult = fileFolderService.search(contextNodeRef, namePattern, true, false, includeSubFolders); + + // No need to ACL check the change token read + String changeToken = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public String doWork() throws Exception + { + return (String) nodeService.getProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN); + } + }, AuthenticationUtil.getSystemUserName()); + + Pair cacheKey = null; + if (changeToken != null) + { + cacheKey = new Pair(userName, changeToken); + this.folderCacheLock.readLock().lock(); + try + { + FolderStatus result = this.folderCache.get(cacheKey); + if (result != null) + { + return result; + } + } + finally + { + this.folderCacheLock.readLock().unlock(); + } + } + List fileInfos = fileFolderService.listFiles(contextNodeRef); + final NavigableMap currentSearch = new TreeMap(); + switch (viewMode) { case MIXED: - result = searchResult; + for (FileInfo fileInfo : fileInfos) + { + currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo); + } break; case ARCHIVE: - for (FileInfo fileInfo : searchResult) + for (FileInfo fileInfo : fileInfos) { if (nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) { - result.add(fileInfo); + currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo); } } break; case VIRTUAL: - for (FileInfo fileInfo : searchResult) + for (FileInfo fileInfo : fileInfos) { if (!nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) { - result.add(fileInfo); + currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo); } } break; } - logger.debug("Found files:" + result.size()); - return result; + 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; + AuthenticationUtil.runAs(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; + } + }, AuthenticationUtil.getSystemUserName()); + } + 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) + { + return oldResult; + } + this.folderCache.put(cacheKey, result); + + logger.debug("Found files:" + currentSearch.size()); + return result; + } + finally + { + this.folderCacheLock.writeLock().unlock(); + } } public void subscribe(AlfrescoImapUser user, String mailbox) @@ -1127,7 +844,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol { logger.debug("Subscribing: " + user + ", " + mailbox); } - AlfrescoImapFolder mailFolder = getFolder(user, mailbox); + AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false); nodeService.removeAspect(mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSUBSCRIBED); } @@ -1137,7 +854,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol { logger.debug("Unsubscribing: " + user + ", " + mailbox); } - AlfrescoImapFolder mailFolder = getFolder(user, mailbox); + AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false); if(mailFolder.getFolderInfo() != null) { logger.debug("Unsubscribing by ASPECT_IMAP_FOLDER_NONSUBSCRIBED"); @@ -1156,12 +873,9 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol * @param messageInfo imap folder info. * @return flags. */ - public synchronized Flags getFlags(FileInfo messageInfo) + public Flags getFlags(FileInfo messageInfo) { Flags flags = new Flags(); - if (nodeService.exists(messageInfo.getNodeRef())) - { - checkForFlaggableAspect(messageInfo.getNodeRef()); Map props = nodeService.getProperties(messageInfo.getNodeRef()); for (QName key : qNameToFlag.keySet()) @@ -1172,7 +886,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol flags.add(qNameToFlag.get(key)); } } - } + return flags; } @@ -1183,7 +897,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol * @param flags flags to set. * @param value value to set. */ - public synchronized void setFlags(FileInfo messageInfo, Flags flags, boolean value) + public void setFlags(FileInfo messageInfo, Flags flags, boolean value) { checkForFlaggableAspect(messageInfo.getNodeRef()); @@ -1203,7 +917,11 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol */ public void setFlag(FileInfo messageInfo, Flag flag, boolean value) { - NodeRef nodeRef = messageInfo.getNodeRef(); + setFlag(messageInfo.getNodeRef(), flag, value); + } + + private void setFlag(NodeRef nodeRef, Flag flag, boolean value) + { checkForFlaggableAspect(nodeRef); AccessStatus status = permissionService.hasPermission(nodeRef, PermissionService.WRITE_PROPERTIES); if (status == AccessStatus.DENIED) @@ -1213,138 +931,90 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } else { - nodeService.setProperty(messageInfo.getNodeRef(), flagToQname.get(flag), value); + nodeService.setProperty(nodeRef, flagToQname.get(flag), value); } + messageCache.remove(nodeRef); } /** * Depend on listSubscribed param, list Mailboxes or list subscribed Mailboxes */ - private List listMailboxes(AlfrescoImapUser user, String mailboxPattern, boolean listSubscribed) + 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(); - - Map mountPoints = getMountPoints(); - - NodeRef mountPoint; - + // List mailboxes that are in mount points - for (String mountPointName : mountPoints.keySet()) + int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + String rootPath = index == -1 ? mailboxPattern : mailboxPattern.substring(0, index); + boolean found = false; + + for (String mountPointName : imapConfigMountPoints.keySet()) { - - mountPoint = mountPoints.get(mountPointName); - FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint); - NodeRef mountParent = nodeService.getParentAssocs(mountPoint).get(0).getParentRef(); - ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode(); - /* FIX for ALF-2793 Reinstated - if (!mailboxPattern.equals("*")) + if (mountPointName.matches(rootPath.replaceAll("[%\\*]", ".*"))) { - mountPoint = mountParent; - } - */ - List folders = expandFolder(mountPoint, mountPoint, user, mailboxPattern, listSubscribed, viewMode); - if (folders != null) - { - for (AlfrescoImapFolder mailFolder : folders) + NodeRef mountPoint = getMountPoint(mountPointName); + if (mountPoint != null) { - AlfrescoImapFolder folder = (AlfrescoImapFolder) mailFolder; - folder.setMountPointName(mountPointName); - folder.setViewMode(viewMode); - folder.setMountParent(mountParent); + FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint); + ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode(); + if (index < 0) + { + String userName = user.getLogin(); + if (!listSubscribed || isSubscribed(mountPointFileInfo, userName)) + { + result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, + isExtractionEnabled(mountPointFileInfo.getNodeRef()), serviceRegistry)); + } + else if (rootPath.endsWith("%") && !expandFolder(mountPoint, user, mountPointName, "%", true, viewMode).isEmpty()) // \NoSelect + { + result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode, + serviceRegistry, false, isExtractionEnabled(mountPointFileInfo.getNodeRef()))); + } + if (rootPath.endsWith("*")) + { + result.addAll(expandFolder(mountPoint, user, mountPointName, "*", listSubscribed, viewMode)); + } + } + else + { + result.addAll(expandFolder(mountPoint, user, mountPointName, + mailboxPattern.substring(index + 1), listSubscribed, viewMode)); + } } - result.addAll(folders); - } - - // Add mount point to the result list - if (mailboxPattern.equals("*")) - { - if ((listSubscribed && isSubscribed(mountPointFileInfo, user.getLogin())) || (!listSubscribed)) + // If we had an exact match, there is no point continuing to search + if (mountPointName.equals(rootPath)) { - result.add( - new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - mountPointFileInfo, - mountPointName, - viewMode, - mountParent, - mountPointName, - isExtractionEnabled(mountPointFileInfo.getNodeRef()), - serviceRegistry)); - } - // \NoSelect - else if (listSubscribed && hasSubscribedChild(mountPointFileInfo, user.getLogin(), viewMode)) - { - result.add( - new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - mountPointFileInfo, - mountPointName, - viewMode, - mountParent, - mountPointName, - serviceRegistry, - false, - isExtractionEnabled(mountPointFileInfo.getNodeRef()))); + found = true; + break; } } - - } // List mailboxes that are in user IMAP Home - NodeRef root = getUserImapHomeRef(user.getLogin()); - List imapFolders = expandFolder(root, root, user, mailboxPattern, listSubscribed, ImapViewMode.ARCHIVE); - - if (imapFolders != null) + if (!found) { - for (AlfrescoImapFolder mailFolder : imapFolders) - { - //AlfrescoImapFolder folder = (AlfrescoImapFolder) mailFolder; - mailFolder.setViewMode(ImapViewMode.ARCHIVE); - mailFolder.setMountParent(root); - } - result.addAll(imapFolders); + NodeRef root = getUserImapHomeRef(user.getLogin()); + result.addAll(expandFolder(root, user, "", mailboxPattern, listSubscribed, ImapViewMode.ARCHIVE)); } - + logger.debug("listMailboxes returning size:" + result.size()); - - StringBuilder prefix = new StringBuilder(128); - prefix.append(ImapConstants.USER_NAMESPACE) - .append(AlfrescoImapConst.HIERARCHY_DELIMITER) - .append(user.getQualifiedMailboxName()) - .append(AlfrescoImapConst.HIERARCHY_DELIMITER); - int prefixLength = prefix.length(); - - for(AlfrescoImapFolder folder : result) - { - String cacheKey = folder.getFullName().substring(prefixLength + 1, folder.getFullName().length() - 1); - logger.debug("[listMailboxes] Adding the cache entry : " + cacheKey); - foldersCache.put(cacheKey, folder); - } return result; - } - + /** - * Get the list of folders + * Recursively search the given root to get a list of folders * - * @param mailboxRoot - * @param root - * @param user - * @param mailboxPattern - * @param listSubscribed - * @param viewMode * @return */ private List expandFolder( - NodeRef mailboxRoot, NodeRef root, AlfrescoImapUser user, + String rootPath, String mailboxPattern, boolean listSubscribed, ImapViewMode viewMode) @@ -1358,8 +1028,6 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); String name = null; - String remainName = null; - if (index < 0) { name = mailboxPattern; @@ -1367,238 +1035,70 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol else { name = mailboxPattern.substring(0, index); - remainName = mailboxPattern.substring(index + 1); } + String rootPathPrefix = rootPath.length() == 0 ? "" : rootPath + AlfrescoImapConst.HIERARCHY_DELIMITER; if (logger.isDebugEnabled()) { logger.debug("Listing mailboxes: name=" + name); } + List fullList = new LinkedList(); + ImapSubFolderFilter filter = new ImapSubFolderFilter(viewMode, name.replace('%', '*')); + List list; + // Only list this folder if we have a wildcard name. Otherwise do a direct lookup by name. + if (name.contains("*") || name.contains("%")) + { + list = fileFolderService.listFolders(root); + } + else + { + NodeRef nodeRef = fileFolderService.searchSimple(root, name); + FileInfo fileInfo; + list = nodeRef == null || !(fileInfo = fileFolderService.getFileInfo(nodeRef)).isFolder() ? Collections.emptyList() : Collections.singletonList(fileInfo); + } + if (index < 0) { - // This is the last level - - if ("*".equals(name)) + // This is the last level + for (FileInfo fileInfo : list) { - // Deep listing of all folders - Collection list = searchDeep(root, viewMode); - if (listSubscribed) + if (!filter.isEnterSubfolder(fileInfo.getNodeRef())) { - list = getSubscribed(list, user.getLogin()); + continue; } - - if (list.size() > 0) + String folderPath = rootPathPrefix + fileInfo.getName(); + String userName = user.getLogin(); + if (!listSubscribed || isSubscribed(fileInfo, userName)) { - return createMailFolderList(user, list, mailboxRoot); + fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode, + isExtractionEnabled(fileInfo.getNodeRef()), serviceRegistry)); + } + else if (name.endsWith("%") && !expandFolder(fileInfo.getNodeRef(), user, folderPath, "%", true, viewMode).isEmpty()) // \NoSelect + { + fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode, + serviceRegistry, false, isExtractionEnabled(fileInfo.getNodeRef()))); + } + if (name.endsWith("*")) + { + fullList.addAll(expandFolder(fileInfo.getNodeRef(), user, folderPath, "*", listSubscribed, viewMode)); } - return null; } - else if (name.endsWith("*")) - { - // Ends with wildcard - List fullList = new LinkedList(); - List list = searchByPattern(root, viewMode, name.replace('%', '*')); - Collection subscribedList = list; - if (listSubscribed) - { - subscribedList = getSubscribed(list, user.getLogin()); - } - - if (list.size() > 0) - { - fullList.addAll(subscribedList); - for (FileInfo fileInfo : list) - { - List childList = searchDeep(fileInfo.getNodeRef(), viewMode); - if (listSubscribed) - { - fullList.addAll(getSubscribed(childList, user.getLogin())); - } - else - { - fullList.addAll(childList); - } - } - return createMailFolderList(user, fullList, mailboxRoot); - } - return null; - } - else if ("%".equals(name)) - { - // Non recursive listing - List subscribedList = new LinkedList(); - List list = searchByPattern(root, viewMode, "*"); - if (listSubscribed) - { - for (FileInfo fileInfo : list) - { - if (isSubscribed(fileInfo, user.getLogin())) - { - // folderName, viewMode, mountPointName will be set in listMailboxes() method - subscribedList.add( - new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - fileInfo, - null, - null, - mailboxRoot, - null, - isExtractionEnabled(fileInfo.getNodeRef()), - serviceRegistry)); - } - // \NoSelect - else if (hasSubscribedChild(fileInfo, user.getLogin(), viewMode)) - { - // folderName, viewMode, mountPointName will be set in listMailboxes() method - subscribedList.add( - new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - fileInfo, - null, - null, - mailboxRoot, - null, - serviceRegistry, - false, - isExtractionEnabled(fileInfo.getNodeRef()))); - } - } - } - else - { - return createMailFolderList(user, list, mailboxRoot); - } - - return subscribedList; - } - else if (name.contains("%") || name.contains("*")) - { - // wild cards in the middle of the name - List list = searchByPattern(root, viewMode, name.replace('%', '*')); - Collection subscribedList = list; - if (listSubscribed) - { - subscribedList = getSubscribed(list, user.getLogin()); - } - - if (subscribedList.size() > 0) - { - return createMailFolderList(user, subscribedList, mailboxRoot); - } - return null; - } - else - { - // No wild cards - List list = searchByPattern(root, viewMode, name); - Collection subscribedList = list; - if (listSubscribed) - { - subscribedList = getSubscribed(list, user.getLogin()); - } - - if (subscribedList.size() > 0) - { - return createMailFolderList(user, subscribedList, mailboxRoot); - } - return null; - } - } - - // If (index != -1) this is not the last level - List result = new LinkedList(); - List list = searchByPattern(root, viewMode, name.replace('%', '*')); - for (FileInfo folder : list) - { - Collection childFolders = expandFolder(mailboxRoot, folder.getNodeRef(), user, remainName, listSubscribed, viewMode); - - if (childFolders != null) - { - result.addAll(childFolders); - } - } - return !result.isEmpty() ? result : null; - } - - /** - * Convert mailpath from IMAP client representation to the alfresco representation view - * (e.g. with default settings - * "getMailPathInRepo(Repository_virtual/Imap Home)" will return "Company Home/Imap Home") - * - * @param mailPath mailbox path in IMAP client - * @return mailbox path in alfresco - */ - private String getMailPathInRepo(String mailPath) - { - if(logger.isDebugEnabled()) - { - logger.debug("[getMailPathInRepo] Path: " + mailPath); - } - String rootFolder; - String remain = ""; - int index = mailPath.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); - if (index > 0) - { - // mail path contains a / - rootFolder = mailPath.substring(0, index); - remain = mailPath.substring(index + 1); } else - { - //mail path is a root folder - rootFolder = mailPath; - } - if (imapConfigMountPoints.keySet().contains(rootFolder)) - { - //Map mountPoints = getMountPoints(); - //NodeRef rootRef = mountPoints.get(rootFolder); - String path = remain; - - if(logger.isDebugEnabled()) + { + // If (index != -1) this is not the last level + for (FileInfo folder : list) { - logger.debug("[getMailPathInRepo] Mounted point returning: " + path); + if (!filter.isEnterSubfolder(folder.getNodeRef())) + { + continue; + } + fullList.addAll(expandFolder(folder.getNodeRef(), user, rootPathPrefix + folder.getName(), + mailboxPattern.substring(index + 1), listSubscribed, viewMode)); } - - return path; } - else - { - if(logger.isDebugEnabled()) - { - logger.debug("[getMailPathInRepo] Not mounted. Returning path as is: " + mailPath); - } - return mailPath; - } - } - - /** - * Return mount point name for the current mailbox. - * - * @param mailboxName mailbox name in IMAP client. - * @return mount point name or null. - */ - private String getMountPointName(String mailboxName) - { - String rootFolder; - int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); - if (index > 0) - { - rootFolder = mailboxName.substring(0, index); - } - else - { - rootFolder = mailboxName; - } - if (imapConfigMountPoints.keySet().contains(rootFolder)) - { - return rootFolder; - } - else - { - return null; - } - + return fullList; } /** @@ -1606,96 +1106,44 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol * * @return Map of mount points. */ - private Map getMountPoints() + private NodeRef getMountPoint(String rootFolder) { - Set mountPointNodeRefs = new HashSet(5); - - Map mountPoints = new HashMap(); final NamespaceService namespaceService = serviceRegistry.getNamespaceService(); final SearchService searchService = serviceRegistry.getSearchService(); - for (final ImapConfigMountPointsBean config : imapConfigMountPoints.values()) + final ImapConfigMountPointsBean config = imapConfigMountPoints.get(rootFolder); + try { - 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() { - // Get node reference. Do it in new transaction to avoid RollBack in case when AccessDeniedException is thrown. - NodeRef nodeRef = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + public NodeRef execute() throws Exception { - public NodeRef execute() throws Exception + try { - 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; + return config.getFolderPath(namespaceService, nodeService, searchService, fileFolderService); } - }, true, true); - - if (nodeRef != null) - { - if (!mountPointNodeRefs.add(nodeRef)) + catch (AccessDeniedException e) { - throw new IllegalArgumentException("A mount point has been defined twice: \n" + " Mount point: " + config); + if (logger.isDebugEnabled()) + { + logger.debug("A mount point is skipped due to Access Dennied. \n" + " Mount point: " + config + "\n" + " User: " + + AuthenticationUtil.getFullyAuthenticatedUser()); + } } - mountPoints.put(config.getMountPointName(), nodeRef); + + return null; } - } - catch (AccessDeniedException e) + }, true, true); + } + catch (AccessDeniedException e) + { + if (logger.isDebugEnabled()) { - if (logger.isDebugEnabled()) - { - logger.debug("A mount point is skipped due to Access Dennied. \n" + " Mount point: " + config + "\n" + " User: " - + AuthenticationUtil.getFullyAuthenticatedUser()); - } + logger.debug("A mount point is skipped due to Access Dennied. \n" + " Mount point: " + config + "\n" + " User: " + + AuthenticationUtil.getFullyAuthenticatedUser()); } } - return mountPoints; - } - - /** - * Get root reference for the specified mailbox - * - * @param mailboxName mailbox name in IMAP client. - * @param userName - * @return root reference for the specified mailbox - */ - public NodeRef getMailboxRootRef(String mailboxName, String userName) - { - String rootFolder; - int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); - if (index > 0) - { - rootFolder = mailboxName.substring(0, index); - } - else - { - rootFolder = mailboxName; - } - - Map imapConfigs = imapConfigMountPoints; - if (imapConfigs.keySet().contains(rootFolder)) - { - Map mountPoints = getMountPoints(); - NodeRef mountRef = mountPoints.get(rootFolder); - logger.debug("getMailboxRootRef mounted, " + mountRef); - return mountRef; - // MER EXPERIMENT - //return nodeService.getParentAssocs(mountRef).get(0).getParentRef(); - } - else - { - NodeRef ret = getUserImapHomeRef(userName); - logger.debug("getMailboxRootRef using userImapHome, " + ret); - return ret; - } + return null; } /** @@ -1705,9 +1153,8 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol * @param userName user name * @return user IMAP home reference and create it if it doesn't exist. */ - private NodeRef getUserImapHomeRef(final String userName) + public NodeRef getUserImapHomeRef(final String userName) { - NodeRef userHome = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { public NodeRef doWork() throws Exception @@ -1741,97 +1188,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol { return !nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSUBSCRIBED); } - - /** - * getSubscribed filters out folders which are not subscribed. - * @param list - * @param userName - * @return collection of subscribed folders. - */ - private Collection getSubscribed(Collection list, String userName) - { - Collection result = new LinkedList(); - - for (FileInfo folderInfo : list) - { - if (isSubscribed(folderInfo, userName)) - { - result.add(folderInfo); - } - } - - return result; - } - - private boolean hasSubscribedChild(FileInfo parent, String userName, ImapViewMode viewMode) - { - List list = searchDeep(parent.getNodeRef(), viewMode); - - for (FileInfo fileInfo : list) - { - if (isSubscribed(fileInfo, userName)) - { - return true; - } - } - - return false; - } - - private List createMailFolderList(AlfrescoImapUser user, Collection list, NodeRef imapUserHomeRef) - { - List result = new LinkedList(); - // XXX : put folders into the cache and get them in next lookups. - // The question is to get a mailBoxName for the cache key... And what if a folders list was changed? - // So, for now keep it as is. - for (FileInfo folderInfo : list) - { - // folderName, viewMode, mountPointName will be set in listSubscribedMailboxes() method - result.add( - new AlfrescoImapFolder( - user.getQualifiedMailboxName(), - folderInfo, - null, - null, - imapUserHomeRef, - null, - isExtractionEnabled(folderInfo.getNodeRef()), - serviceRegistry)); - } - - return result; - - } - - /** - * Return view mode ("virtual", "archive" or "mixed") for specified mailbox. - * - * @param mailboxName name of the mailbox in IMAP client. - * @return view mode of the specified mailbox. - */ - private ImapViewMode getViewMode(String mailboxName) - { - String rootFolder; - int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); - if (index > 0) - { - rootFolder = mailboxName.substring(0, index); - } - else - { - rootFolder = mailboxName; - } - if (imapConfigMountPoints.keySet().contains(rootFolder)) - { - return imapConfigMountPoints.get(rootFolder).getMode(); - } - else - { - return ImapViewMode.ARCHIVE; - } - } - private String getCurrentUser() { return AuthenticationUtil.getFullyAuthenticatedUser(); @@ -2095,7 +1452,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol ImapSubFolderFilter(ImapViewMode imapViewMode) { this.imapViewMode = imapViewMode; - this.typesToExclude = serviceRegistry.getDictionaryService().getSubTypes(SiteModel.TYPE_SITE, true); + this.typesToExclude = ImapServiceImpl.this.serviceRegistry.getDictionaryService().getSubTypes(SiteModel.TYPE_SITE, true); this.favs = getFavouriteSites(getCurrentUser()); } @@ -2108,11 +1465,15 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol @Override public boolean isEnterSubfolder(ChildAssociationRef subfolderRef) { - NodeRef folder = subfolderRef.getChildRef(); + return isEnterSubfolder(subfolderRef.getChildRef()); + } + + public boolean isEnterSubfolder(NodeRef folder) + { + String name = (String) nodeService.getProperty(folder, ContentModel.PROP_NAME); if (mailboxPattern != null) { - logger.debug("Child QName: " + subfolderRef.getQName()); - String name = (String) nodeService.getProperty(folder, ContentModel.PROP_NAME); + logger.debug("Child name: " + name); if (logger.isDebugEnabled()) { logger.debug("Folder name: " + name + ". Pattern: " + mailboxPattern + ". Matches: " + name.matches(mailboxPattern)); @@ -2130,12 +1491,12 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol */ if (favs.contains(folder)) { - logger.debug("[ImapSubFolderFilter] (VIRTUAL) including fav site folder :" + subfolderRef.getQName()); + logger.debug("[ImapSubFolderFilter] (VIRTUAL) including fav site folder :" + name); return true; } else { - logger.debug("[ImapSubFolderFilter] (VIRTUAL) excluding non fav site folder :" + subfolderRef.getQName()); + logger.debug("[ImapSubFolderFilter] (VIRTUAL) excluding non fav site folder :" + name); return false; } } @@ -2144,7 +1505,7 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol /** * IN ARCHIVE MODE we don't display folders for any SITES, regardless of whether they are favourites. */ - logger.debug("[ImapSubFolderFilter] (ARCHIVE) excluding site folder :" + subfolderRef.getQName()); + logger.debug("[ImapSubFolderFilter] (ARCHIVE) excluding site folder :" + name); return false; } } @@ -2153,192 +1514,160 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } + 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(ChildAssociationRef childAssocRef, boolean isNewNode) { - // Add a listener once, when a lots of messsages were created/moved into the folder - if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) + NodeRef childNodeRef = childAssocRef.getChildRef(); + + if (this.serviceRegistry.getDictionaryService().isSubClass(this.nodeService.getType(childNodeRef), ContentModel.TYPE_CONTENT)) { - AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(childAssocRef.getParentRef(), nodeService)); - AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); + 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. UIDVALIDITY will be changed."); + logger.debug("[onCreateChildAssociation] Association " + childAssocRef + " created. CHANGETOKEN will be changed."); } } + @Override public void onDeleteChildAssociation(ChildAssociationRef childAssocRef) { - // Add a listener once, when a lots of messsages were created/moved into the folder - if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) + NodeRef childNodeRef = childAssocRef.getChildRef(); + if (this.serviceRegistry.getDictionaryService().isSubClass(this.nodeService.getType(childNodeRef), ContentModel.TYPE_CONTENT)) { - AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(childAssocRef.getParentRef(), nodeService)); - AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); + // Force generation of a new change token + getUidValidityTransactionListener(childAssocRef.getParentRef()); + + // Remove the message from the cache + this.messageCache.remove(childNodeRef); } if (logger.isDebugEnabled()) { - logger.debug("[onDeleteChildAssociation] Association " + childAssocRef + " removed. UIDVALIDITY will be changed."); + logger.debug("[onDeleteChildAssociation] Association " + childAssocRef + " created. CHANGETOKEN will be changed."); } } + @Override + public void onUpdateProperties(NodeRef nodeRef, Map before, Map after) + { + for (ChildAssociationRef parentAssoc : nodeService.getParentAssocs(nodeRef)) + { + NodeRef folderRef = parentAssoc.getParentRef(); + if (this.nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER)) + { + this.messageCache.remove(nodeRef); + // Force generation of a new change token + getUidValidityTransactionListener(folderRef); + } + } + } + + @Override public void beforeDeleteNode(NodeRef nodeRef) { - - NodeRef parentNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); - if (ContentModel.TYPE_FOLDER.equals(nodeService.getType(nodeRef))) + for (ChildAssociationRef parentAssoc : nodeService.getParentAssocs(nodeRef)) { - // If a node is a folder, we need to remove its' cache with its' children cache as well - invalidateFolderCacheByNodeRef(nodeRef, true); - } - else if (ContentModel.TYPE_CONTENT.equals(nodeService.getType(nodeRef))) - { - // If a node is a content, it is simpler to remove its' parent cache - // to avoid of deal with folder messages cache - invalidateFolderCacheByNodeRef(parentNodeRef, false); - } - else - { - return; - } - // Add a listener once, when a lots of messsages were created/moved into the folder - if (AlfrescoTransactionSupport.getResource(UIDVALIDITY_LISTENER_ALREADY_BOUND) == null) - { - AlfrescoTransactionSupport.bindListener(new UidValidityTransactionListener(parentNodeRef, nodeService)); - AlfrescoTransactionSupport.bindResource(UIDVALIDITY_LISTENER_ALREADY_BOUND, true); - } - if (logger.isDebugEnabled()) - { - logger.debug("[beforeDeleteNode] Node " + nodeRef + " going to be removed. UIDVALIDITY will be changed for " + parentNodeRef); + NodeRef folderRef = parentAssoc.getParentRef(); + if (this.nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER)) + { + this.messageCache.remove(nodeRef); + + // Force generation of a new change token + getUidValidityTransactionListener(folderRef); + } } } - + private class UidValidityTransactionListener extends TransactionListenerAdapter { - - private RunAsWork work; - - UidValidityTransactionListener(NodeRef folderNodeRef, NodeService nodeService) - { - this.work = new IncrementUidValidityWork(folderNodeRef, nodeService); - } - - @Override - public void afterCommit() - { - AuthenticationUtil.runAs(this.work, AuthenticationUtil.getSystemUserName()); - } - - } - - private class IncrementUidValidityWork implements RunAsWork - { + // 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; - public IncrementUidValidityWork(NodeRef folderNodeRef, NodeService nodeService) + public UidValidityTransactionListener(NodeRef folderNodeRef, NodeService nodeService) { this.folderNodeRef = folderNodeRef; this.nodeService = nodeService; } + + 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 Long doWork() throws Exception + public void beforeCommit(boolean readOnly) { - RetryingTransactionHelper txnHelper = serviceRegistry.getRetryingTransactionHelper(); - return txnHelper.doInTransaction(new RetryingTransactionCallback(){ + if (readOnly) + { + return; + } + AuthenticationUtil.runAs(new RunAsWork() + { @Override - public Long execute() throws Throwable + public Void doWork() throws Exception { - long modifDate = new Date().getTime(); - - if (!IncrementUidValidityWork.this.nodeService.hasAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER)) + if (UidValidityTransactionListener.this.minUid != null) { - Map aspectProperties = new HashMap(1, 1); - aspectProperties.put(ImapModel.PROP_UIDVALIDITY, modifDate); - IncrementUidValidityWork.this.nodeService.addAspect(folderNodeRef, ImapModel.ASPECT_IMAP_FOLDER, aspectProperties); + 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 (oldMax == null || UidValidityTransactionListener.this.minUid < oldMax) + { + UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate); + if (logger.isDebugEnabled()) + { + logger.debug("UIDVALIDITY was modified for " + folderNodeRef); + } + } + UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_MAXUID, UidValidityTransactionListener.this.maxUid); + if (logger.isDebugEnabled()) + { + logger.debug("MAXUID was modified for " + folderNodeRef); + } } - else - { - IncrementUidValidityWork.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate); - } - if (logger.isDebugEnabled()) - { - logger.debug("UIDVALIDITY was modified for " + folderNodeRef); - } - return modifDate; - } - - }, false, true); + UidValidityTransactionListener.this.nodeService.setProperty(folderNodeRef, ImapModel.PROP_CHANGE_TOKEN, changeToken); + return null; + } + }, AuthenticationUtil.getSystemUserName()); } + } - } - - - private void invalidateFolderCacheByNodeRef(NodeRef folderNodeRef, boolean invalidateChildren) - { - if (logger.isDebugEnabled()) - { - if (invalidateChildren) - { - logger.debug("[invalidateFolderCacheByNodeRef] Invalidate cache entries for " + folderNodeRef); - } - else - { - logger.debug("[invalidateFolderCacheByNodeRef] Invalidate cache entries for " + folderNodeRef + " and children"); - } - } - - if (invalidateChildren) - { - SimpleCache foldersCache = getFoldersCache(); - List toRemove = new LinkedList(); - for(Serializable name : foldersCache.getKeys()) - { - AlfrescoImapFolder folder = (AlfrescoImapFolder) foldersCache.get(name); - if (folder == null || folderNodeRef.equals(folder.getFolderInfo().getNodeRef())) - { - toRemove.add(name); - break; - } - } - if (toRemove.size() > 0) - { - String rootName = (String) toRemove.get(0); - for(Serializable name : foldersCache.getKeys()) - { - if (((String) name).startsWith(rootName)) - { - toRemove.add(name); - } - } - if (logger.isDebugEnabled()) - { - logger.debug("Caches to invalidate: " + toRemove.toString()); - } - for(Serializable name : toRemove) - { - foldersCache.remove(name); - } - } - } - else - { - SimpleCache foldersCache = getFoldersCache(); - for(Serializable name : foldersCache.getKeys()) - { - AlfrescoImapFolder folder = (AlfrescoImapFolder) foldersCache.get(name); - if (folder == null || folderNodeRef.equals(folder.getFolderInfo().getNodeRef())) - { - foldersCache.remove(name); - break; - } - } - } - } - /** * Return true if provided nodeRef is in Sites/.../documentlibrary */ @@ -2536,4 +1865,36 @@ public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPol } } } + + 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; + } + } } diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java b/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java index c82b21c60b..5bcf42cbbb 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImplCacheTest.java @@ -10,6 +10,7 @@ import junit.framework.TestCase; import org.alfresco.model.ContentModel; import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory; import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; @@ -21,6 +22,7 @@ import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.config.RepositoryFolderConfigBean; import org.springframework.context.ApplicationContext; @@ -47,6 +49,8 @@ public class ImapServiceImplCacheTest extends TestCase private NamespaceService namespaceService; private FileFolderService fileFolderService; private ContentService contentService; + private TransactionService transactionService; + private FileInfo oldFile; private ImapService imapService; @@ -63,6 +67,7 @@ public class ImapServiceImplCacheTest extends TestCase namespaceService = serviceRegistry.getNamespaceService(); fileFolderService = serviceRegistry.getFileFolderService(); contentService = serviceRegistry.getContentService(); + transactionService = serviceRegistry.getTransactionService(); authenticationService.authenticate(USER_NAME, USER_PASSWORD.toCharArray()); @@ -78,12 +83,13 @@ public class ImapServiceImplCacheTest extends TestCase ChildApplicationContextFactory imap = (ChildApplicationContextFactory) ctx.getBean("imap"); ApplicationContext imapCtx = imap.getApplicationContext(); - ImapServiceImpl imapServiceImpl = (ImapServiceImpl)imapCtx.getBean("imapService"); + final ImapServiceImpl imapServiceImpl = (ImapServiceImpl)imapCtx.getBean("imapService"); // Creating IMAP test folder for IMAP root LinkedList folders = new LinkedList(); folders.add(TEST_IMAP_FOLDER_NAME); - FileFolderServiceImpl.makeFolders(fileFolderService, companyHomeNodeRef, folders, ContentModel.TYPE_FOLDER); + FileInfo folder = FileFolderServiceImpl.makeFolders(fileFolderService, companyHomeNodeRef, folders, ContentModel.TYPE_FOLDER); + oldFile = fileFolderService.create(folder.getNodeRef(), "oldFile", ContentModel.TYPE_CONTENT); // Setting IMAP root RepositoryFolderConfigBean imapHome = new RepositoryFolderConfigBean(); @@ -93,7 +99,15 @@ public class ImapServiceImplCacheTest extends TestCase imapServiceImpl.setImapHome(imapHome); // Starting IMAP - imapServiceImpl.startup(); + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + imapServiceImpl.startup(); + return null; + } + }); nodeRefs = searchService.selectNodes(storeRootNodeRef, companyHomePathInStore + "/" + NamespaceService.CONTENT_MODEL_PREFIX + ":" + TEST_IMAP_FOLDER_NAME, @@ -136,31 +150,33 @@ public class ImapServiceImplCacheTest extends TestCase // Create content within 'Alfresco IMAP/aaa/ALF9361' createTestContent(localRootFolder, contentItemsCount); // Load the cache - imapService.listMailboxes(localUser, "*"); - imapService.listSubscribedMailboxes(localUser, "*"); + imapService.listMailboxes(localUser, "*", false); + imapService.listMailboxes(localUser, "*", true); // Get the folder to examine - AlfrescoImapFolder folder = imapService.getFolder(localUser, mailbox); + AlfrescoImapFolder folder = imapService.getOrCreateMailbox(localUser, mailbox, true, false); // Check the folder exist via IMAP assertNotNull("Folder wasn't successfully gotten from IMAP", folder); assertEquals(contentItemsCount, folder.getMessageCount()); // Check UIDVALIDITY long uidValidityBefore = folder.getUidValidity(); + // Move in an old file with a smaller UID + fileFolderService.move(oldFile.getNodeRef(), folder.getFolderInfo().getNodeRef(), folder.getName()); + // Get the folder once more and check it was changed since an old child was moved in + folder = imapService.getOrCreateMailbox(localUser, mailbox, true, false); + // Content count should be increased + assertEquals(++contentItemsCount, folder.getMessageCount()); + long uidValidity = folder.getUidValidity(); + assertTrue("UIDVALIDITY wasn't incremented", (uidValidity - uidValidityBefore) > 0); // Delete first childMailbox 'ALF9361/ALF9361_0' //System.out.println(" --------------------- DELETE FOLDER --------------------"); //System.out.println(" Parent " + localRootFolder.getNodeRef()); fileFolderService.delete(subFolders.get(0).getNodeRef()); - // Get the folder once more and check it was changed since child was removed - folder = imapService.getFolder(localUser, mailbox); - // Content count should be the same since we havn't deleted a content yet - assertEquals(contentItemsCount, folder.getMessageCount()); - long uidValidity = folder.getUidValidity(); - assertTrue("UIDVALIDITY wasn't incremented", (uidValidity - uidValidityBefore) > 0); uidValidityBefore = uidValidity; // Try to get deleted child try { String subFolderName = mailbox + AlfrescoImapConst.HIERARCHY_DELIMITER + folderName + "_0"; - folder = imapService.getFolder(localUser, subFolderName); + folder = imapService.getOrCreateMailbox(localUser, subFolderName, true, false); fail("The folder still in the cache"); } catch (RuntimeException e) @@ -173,7 +189,7 @@ public class ImapServiceImplCacheTest extends TestCase try { String subSubFolderName = mailbox + AlfrescoImapConst.HIERARCHY_DELIMITER + mailbox + "_0" + AlfrescoImapConst.HIERARCHY_DELIMITER + "sub_0"; - folder = imapService.getFolder(localUser, subSubFolderName); + folder = imapService.getOrCreateMailbox(localUser, subSubFolderName, true, false); fail("The folder still in the cache"); } catch (RuntimeException e) @@ -181,7 +197,7 @@ public class ImapServiceImplCacheTest extends TestCase // expected } // Do manipulations with a content in the folder to check the cache behaviour - folder = imapService.getFolder(localUser, mailbox); + folder = imapService.getOrCreateMailbox(localUser, mailbox, true, false); SimpleStoredMessage message = folder.getMessages().get(0); AbstractMimeMessage alfrescoMessage = (AbstractMimeMessage) message.getMimeMessage(); long uid = message.getUid(); @@ -191,10 +207,7 @@ public class ImapServiceImplCacheTest extends TestCase fileFolderService.delete(alfrescoMessage.getMessageInfo().getNodeRef()); // Get a folder once again. We expect that the folder would be retrieved from the repo, // since its' cache should be invalidated - folder = imapService.getFolder(localUser, mailbox); - // Get UIDVALIDITY. It should be changed, since we removed a message form the mailbox. - uidValidity = folder.getUidValidity(); - assertTrue("UIDVALIDITY wasn't incremented", (uidValidity - uidValidityBefore) > 0); + folder = imapService.getOrCreateMailbox(localUser, mailbox, true, false); // Additional check whether messages cache is valid. Messages cache should be recreated //with the new inctance of AlfrescoImapMessage assertTrue("Messages cache is stale", contentItemsCount > folder.getMessageCount()); diff --git a/source/java/org/alfresco/repo/imap/ImapServiceImplTest.java b/source/java/org/alfresco/repo/imap/ImapServiceImplTest.java index 32e47ba5cc..e0c4369141 100644 --- a/source/java/org/alfresco/repo/imap/ImapServiceImplTest.java +++ b/source/java/org/alfresco/repo/imap/ImapServiceImplTest.java @@ -25,6 +25,7 @@ import java.io.Serializable; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Properties; import javax.mail.Flags; @@ -229,9 +230,11 @@ public class ImapServiceImplTest extends TestCase private boolean checkMailbox(AlfrescoImapUser user, String mailboxName) { - AlfrescoImapFolder mailFolder = (AlfrescoImapFolder)imapService.getFolder(user, mailboxName); - - if (mailFolder.getFolderInfo() == null) + try + { + imapService.getOrCreateMailbox(user, mailboxName, true, false); + } + catch (AlfrescoRuntimeException e) { return false; } @@ -240,7 +243,7 @@ public class ImapServiceImplTest extends TestCase private boolean checkSubscribedMailbox(AlfrescoImapUser user, String mailboxName) { - List aifs = imapService.listSubscribedMailboxes(user, mailboxName); + List aifs = imapService.listMailboxes(user, mailboxName, true); boolean present = false; for (AlfrescoImapFolder aif : aifs) { @@ -262,15 +265,15 @@ public class ImapServiceImplTest extends TestCase public void testGetFolder() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); assertTrue(checkMailbox(user, MAILBOX_NAME_A)); } public void testListMailbox() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); - imapService.createMailbox(user, MAILBOX_NAME_B); - List mf = imapService.listMailboxes(user, MAILBOX_PATTERN); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_B, false, true); + List mf = imapService.listMailboxes(user, MAILBOX_PATTERN, false); assertEquals(2, mf.size()); boolean foundA = false; @@ -291,33 +294,33 @@ public class ImapServiceImplTest extends TestCase assertTrue("folder A found", foundA); assertTrue("folder B found", foundB); - mf = imapService.listMailboxes(user, MAILBOX_PATTERN); + mf = imapService.listMailboxes(user, MAILBOX_PATTERN, false); assertEquals("can't repeat the listing of folders", 2, mf.size()); - mf = imapService.listMailboxes(user, MAILBOX_PATTERN); + mf = imapService.listMailboxes(user, MAILBOX_PATTERN, false); assertEquals("can't repeat the listing of folders", 2, mf.size()); /** * The new mailboxes should be subscribed? */ - List aif = imapService.listSubscribedMailboxes(user, MAILBOX_PATTERN); + List aif = imapService.listMailboxes(user, MAILBOX_PATTERN, true); assertEquals("not subscribed to two mailboxes", 2, aif.size()); /** * Unsubscribe to one of the mailboxes. */ imapService.unsubscribe(user, MAILBOX_NAME_B); - List aif2 = imapService.listSubscribedMailboxes(user, MAILBOX_PATTERN); + List aif2 = imapService.listMailboxes(user, MAILBOX_PATTERN, true); assertEquals("not subscribed to one mailbox", 1, aif2.size()); } public void testListSubscribedMailbox() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); - imapService.createMailbox(user, MAILBOX_NAME_B); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_B, false, true); imapService.subscribe(user, MAILBOX_NAME_A); imapService.subscribe(user, MAILBOX_NAME_B); - List aif = imapService.listSubscribedMailboxes(user, MAILBOX_PATTERN); + List aif = imapService.listMailboxes(user, MAILBOX_PATTERN, true); assertEquals(aif.size(), 2); assertTrue("Can't subscribe mailbox A", checkSubscribedMailbox(user, MAILBOX_NAME_A)); @@ -326,16 +329,16 @@ public class ImapServiceImplTest extends TestCase public void testCreateMailbox() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); assertTrue("Mailbox isn't created", checkMailbox(user, MAILBOX_NAME_A)); } public void testDuplicateMailboxes() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); try { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); fail("Duplicate Mailbox was created"); } catch (AlfrescoRuntimeException e) @@ -347,7 +350,7 @@ public class ImapServiceImplTest extends TestCase public void testRenameMailbox() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); imapService.renameMailbox(user, MAILBOX_NAME_A, MAILBOX_NAME_B); assertFalse("Can't rename mailbox", checkMailbox(user, MAILBOX_NAME_A)); assertTrue("Can't rename mailbox", checkMailbox(user, MAILBOX_NAME_B)); @@ -355,8 +358,8 @@ public class ImapServiceImplTest extends TestCase public void testRenameMailboxDuplicate() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); - imapService.createMailbox(user, MAILBOX_NAME_B); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_B, false, true); try { imapService.renameMailbox(user, MAILBOX_NAME_A, MAILBOX_NAME_B); @@ -370,7 +373,7 @@ public class ImapServiceImplTest extends TestCase public void testDeleteMailbox() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_B); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_B, false, true); imapService.deleteMailbox(user, MAILBOX_NAME_B); assertFalse("Can't delete mailbox", checkMailbox(user, MAILBOX_NAME_B)); } @@ -424,7 +427,7 @@ public class ImapServiceImplTest extends TestCase public void testSubscribe() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); imapService.subscribe(user, MAILBOX_NAME_A); assertTrue("Can't subscribe mailbox", checkSubscribedMailbox(user, MAILBOX_NAME_A)); @@ -432,7 +435,7 @@ public class ImapServiceImplTest extends TestCase public void testUnsubscribe() throws Exception { - imapService.createMailbox(user, MAILBOX_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_NAME_A, false, true); imapService.subscribe(user, MAILBOX_NAME_A); imapService.unsubscribe(user, MAILBOX_NAME_A); // TODO MER 21/05/2010 : line below looks like a bug to me. @@ -453,10 +456,10 @@ public class ImapServiceImplTest extends TestCase public void testSetFlags() throws Exception { - List fis = imapService.searchMails(testImapFolderNodeRef, ImapViewMode.ARCHIVE); + NavigableMap fis = imapService.getFolderStatus(authenticationService.getCurrentUserName(), testImapFolderNodeRef, ImapViewMode.ARCHIVE).search; if (fis != null && fis.size() > 0) { - FileInfo messageFileInfo = fis.get(0); + FileInfo messageFileInfo = fis.firstEntry().getValue(); try { setFlags(messageFileInfo); @@ -486,10 +489,10 @@ public class ImapServiceImplTest extends TestCase public void testSetFlag() throws Exception { - List fis = imapService.searchMails(testImapFolderNodeRef, ImapViewMode.ARCHIVE); + NavigableMap fis = imapService.getFolderStatus(authenticationService.getCurrentUserName(), testImapFolderNodeRef, ImapViewMode.ARCHIVE).search; if (fis != null && fis.size() > 0) { - FileInfo messageFileInfo = fis.get(0); + FileInfo messageFileInfo = fis.firstEntry().getValue(); reauthenticate(USER_NAME, USER_PASSWORD); @@ -506,10 +509,10 @@ public class ImapServiceImplTest extends TestCase public void testGetFlags() throws Exception { - List fis = imapService.searchMails(testImapFolderNodeRef, ImapViewMode.ARCHIVE); + NavigableMap fis = imapService.getFolderStatus(authenticationService.getCurrentUserName(), testImapFolderNodeRef, ImapViewMode.ARCHIVE).search; if (fis != null && fis.size() > 0) { - FileInfo messageFileInfo = fis.get(0); + FileInfo messageFileInfo = fis.firstEntry().getValue(); reauthenticate(USER_NAME, USER_PASSWORD); @@ -526,13 +529,13 @@ public class ImapServiceImplTest extends TestCase public void testRenameAccentedMailbox() throws Exception { - String MAILBOX_ACCENTED_NAME_A = "H�tel"; - String MAILBOX_ACCENTED_NAME_B = "H�telXX"; + String MAILBOX_ACCENTED_NAME_A = "Hôtel"; + String MAILBOX_ACCENTED_NAME_B = "HôtelXX"; - imapService.createMailbox(user, MAILBOX_ACCENTED_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_ACCENTED_NAME_A, false, true); imapService.deleteMailbox(user, MAILBOX_ACCENTED_NAME_A); - imapService.createMailbox(user, MAILBOX_ACCENTED_NAME_A); + imapService.getOrCreateMailbox(user, MAILBOX_ACCENTED_NAME_A, false, true); imapService.renameMailbox(user, MAILBOX_ACCENTED_NAME_A, MAILBOX_ACCENTED_NAME_B); assertFalse("Can't rename mailbox", checkMailbox(user, MAILBOX_ACCENTED_NAME_A)); assertTrue("Can't rename mailbox", checkMailbox(user, MAILBOX_ACCENTED_NAME_B)); diff --git a/source/java/org/alfresco/repo/imap/LoadTester.java b/source/java/org/alfresco/repo/imap/LoadTester.java index 75e295b0c2..eda81042d5 100644 --- a/source/java/org/alfresco/repo/imap/LoadTester.java +++ b/source/java/org/alfresco/repo/imap/LoadTester.java @@ -172,19 +172,19 @@ public class LoadTester extends TestCase false); // Used to create User's folder - NodeRef userFolderRef = imapService.getMailboxRootRef(TEST_DATA_FOLDER_NAME, anotherUserName); + NodeRef userFolderRef = imapService.getUserImapHomeRef(anotherUserName); permissionService.setPermission(userFolderRef, anotherUserName, PermissionService.ALL_PERMISSIONS, true); importTestData("imap/load_test_data.acp", userFolderRef); reauthenticate(anotherUserName, anotherUserName); - AlfrescoImapFolder testDataFolder = imapService.getFolder(user, TEST_DATA_FOLDER_NAME); + AlfrescoImapFolder testDataFolder = imapService.getOrCreateMailbox(user, TEST_DATA_FOLDER_NAME, true, false); SimpleStoredMessage m = testDataFolder.getMessages().get(0); m = testDataFolder.getMessage(m.getUid()); - AlfrescoImapFolder folder = imapService.createMailbox(user, TEST_FOLDER_NAME); + AlfrescoImapFolder folder = imapService.getOrCreateMailbox(user, TEST_FOLDER_NAME, false, true); logger.info("Creating folders..."); long t = System.currentTimeMillis(); @@ -227,7 +227,7 @@ public class LoadTester extends TestCase logger.info("Listing folders..."); long t = System.currentTimeMillis(); - List list = imapService.listMailboxes(user, TEST_FOLDER_NAME + "*"); + List list = imapService.listMailboxes(user, TEST_FOLDER_NAME + "*", false); t = System.currentTimeMillis() - t; logger.info("List time: " + t + " ms (" + t/1000 + " s)"); diff --git a/source/java/org/alfresco/repo/imap/RemoteLoadTester.java b/source/java/org/alfresco/repo/imap/RemoteLoadTester.java index eff32e3c8b..c4ae236d1a 100644 --- a/source/java/org/alfresco/repo/imap/RemoteLoadTester.java +++ b/source/java/org/alfresco/repo/imap/RemoteLoadTester.java @@ -27,6 +27,7 @@ import javax.mail.BodyPart; import javax.mail.Folder; import javax.mail.Message; import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; import javax.mail.Part; import javax.mail.Session; import javax.mail.Store; @@ -46,6 +47,16 @@ public class RemoteLoadTester extends TestCase private static final String USER_NAME = "test_imap_user"; private static final String USER_PASSWORD = "test_imap_user"; private static final String TEST_FOLDER_NAME = "test_imap1000"; + + private static final String ADMIN_USER_NAME = "admin"; + private static String REMOTE_HOST = "127.0.0.1"; + + public static void main(String[] args) + { + if (args.length > 0) + REMOTE_HOST = args[0]; + new RemoteLoadTester().testListSequence(); + } @Override public void setUp() throws Exception @@ -54,17 +65,75 @@ public class RemoteLoadTester extends TestCase public void tearDown() throws Exception { - } + public void testListSequence() + { + System.out.println(String.format("Connecting to remote server '%s'", REMOTE_HOST)); + Properties props = System.getProperties(); + props.setProperty("mail.imap.partialfetch", "false"); + Session session = Session.getDefaultInstance(props, null); + + Store store = null; + long startTime = 0; + long endTime = 0; + try + { + store = session.getStore("imap"); + store.connect(REMOTE_HOST, ADMIN_USER_NAME, ADMIN_USER_NAME); + Folder[] folders = null; + + startTime = System.currentTimeMillis(); + folders = store.getDefaultFolder().list(""); + endTime = System.currentTimeMillis(); + System.out.println(String.format("LIST '', folders.length = %d, execTime = %d sec", folders.length, (endTime - startTime)/1000)); + + startTime = System.currentTimeMillis(); + folders = store.getDefaultFolder().list("*"); + endTime = System.currentTimeMillis(); + System.out.println(String.format("LIST *, folders.length = %d, execTime = %d sec", folders.length, (endTime - startTime)/1000)); + + startTime = System.currentTimeMillis(); + folders = store.getDefaultFolder().listSubscribed("*"); + endTime = System.currentTimeMillis(); + System.out.println(String.format("LSUB *, folders.length = %d, execTime = %d sec", folders.length, (endTime - startTime)/1000)); + + startTime = System.currentTimeMillis(); + for (Folder folder : folders) + { + folder.getMessageCount(); + //Folder f = store.getFolder(folder.getFullName()); + } + endTime = System.currentTimeMillis(); + System.out.println(String.format("Folders Loop, folders.length = %d, execTime = %d sec", folders.length, (endTime - startTime)/1000)); + + } + catch (NoSuchProviderException e) + { + e.printStackTrace(); + } + catch (MessagingException e) + { + e.printStackTrace(); + } + finally + { + try + { + store.close(); + } + catch (MessagingException e) + { + System.err.println(e.getMessage()); + } + } + } public void testMailbox() { logger.info("Getting folder..."); long t = System.currentTimeMillis(); - String host = "localhost"; - // Create empty properties Properties props = new Properties(); props.setProperty("mail.imap.partialfetch", "false"); @@ -78,7 +147,7 @@ public class RemoteLoadTester extends TestCase { // Get the store store = session.getStore("imap"); - store.connect(host, USER_NAME, USER_PASSWORD); + store.connect(REMOTE_HOST, USER_NAME, USER_PASSWORD); // Get folder folder = store.getFolder(TEST_FOLDER_NAME); diff --git a/source/java/org/alfresco/repo/jscript/Imap.java b/source/java/org/alfresco/repo/jscript/Imap.java index fbafa8bb2b..a425d7cd07 100644 --- a/source/java/org/alfresco/repo/jscript/Imap.java +++ b/source/java/org/alfresco/repo/jscript/Imap.java @@ -22,7 +22,6 @@ import org.alfresco.repo.model.Repository; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; -import org.springframework.extensions.surf.util.ParameterCheck; public final class Imap extends BaseScopableProcessorExtension { @@ -74,13 +73,12 @@ public final class Imap extends BaseScopableProcessorExtension /** * Searches NodeRef to the IMAP home for specified user * - * @param mailboxName the name of the mailbox * @param userName the name of the user */ - public ScriptNode getImapHomeRef(String mailboxName, String userName) + public ScriptNode getImapHomeRef(String userName) { ScriptNode result = null; - NodeRef nodeRef = services.getImapService().getMailboxRootRef(mailboxName, userName); + NodeRef nodeRef = services.getImapService().getUserImapHomeRef(userName); if (nodeRef != null) { result = new ScriptNode(nodeRef, this.services, getScope()); diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java index 484edc3ee5..6067eb2a3a 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImpl.java @@ -1321,6 +1321,11 @@ public class FileFolderServiceImpl implements FileFolderService } public FileInfo resolveNamePath(NodeRef rootNodeRef, List pathElements) throws FileNotFoundException + { + return resolveNamePath(rootNodeRef, pathElements, true); + } + + public FileInfo resolveNamePath(NodeRef rootNodeRef, List pathElements, boolean mustExist) throws FileNotFoundException { if (pathElements.size() == 0) { @@ -1347,9 +1352,14 @@ public class FileFolderServiceImpl implements FileFolderService NodeRef fileNodeRef = searchSimple(parentNodeRef, pathElement); if (fileNodeRef == null) { - StringBuilder sb = new StringBuilder(128); - sb.append("File not found: " + currentPath); - throw new FileNotFoundException(sb.toString()); + if (mustExist) + { + throw new FileNotFoundException("File not found: " + currentPath); + } + else + { + return null; + } } FileInfo result = getFileInfo(fileNodeRef); // found it diff --git a/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java b/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java index 1bf5c35bc1..3dcd619dd2 100644 --- a/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java +++ b/source/java/org/alfresco/repo/node/index/AbstractReindexComponent.java @@ -440,17 +440,22 @@ public abstract class AbstractReindexComponent implements IndexRecovery // Check if the txn ID is present in every applicable store's index for (Map.Entry> entry : storeStatusMap.entrySet()) { - StoreRef storeRef = entry.getKey(); List storeStatuses = entry.getValue(); + if (storeStatuses.isEmpty()) + { + // Nothing to check + continue; + } + StoreRef storeRef = entry.getKey(); // Establish the number of deletes and updates for this storeRef - int deleteCount = 0; + List deletedNodes = new LinkedList(); int updateCount = 0; for (NodeRef.Status nodeStatus : storeStatuses) { if (nodeStatus.isDeleted()) { - deleteCount++; + deletedNodes.add(nodeStatus.getNodeRef()); } else { @@ -458,6 +463,15 @@ public abstract class AbstractReindexComponent implements IndexRecovery } } + // There were deleted nodes. Check that all the deleted nodes (including containers) were removed from the + // index - otherwise it is out of date. If all nodes have been removed from the index then the result is that the index is OK + // ETWOTWO-1387 + // ALF-1989 - even if the nodes have not been found it is no good to use for AUTO index checking + if (!deletedNodes.isEmpty() && !haveNodesBeenRemovedFromIndex(storeRef, deletedNodes, txn)) + { + result = InIndex.NO; + break; + } if (updateCount > 0) { // Check the index @@ -472,15 +486,6 @@ public abstract class AbstractReindexComponent implements IndexRecovery break; } } - // There were deleted nodes only. Check that all the deleted nodes were removed from the index otherwise it - // is out of date. If all nodes have been removed from the index then the result is that the index is OK - // ETWOTWO-1387 - // ALF-1989 - even if the nodes have not been found it is no good to use for AUTO index checking - else if (deleteCount > 0 && !haveNodesBeenRemovedFromIndex(storeRef, storeStatuses, txn)) - { - result = InIndex.NO; - break; - } } // done @@ -631,14 +636,13 @@ public abstract class AbstractReindexComponent implements IndexRecovery } } - private boolean haveNodesBeenRemovedFromIndex(final StoreRef storeRef, List nodeStatuses, final Transaction txn) + private boolean haveNodesBeenRemovedFromIndex(final StoreRef storeRef, List nodeRefs, final Transaction txn) { final Long txnId = txn.getId(); // there have been deletes, so we have to ensure that none of the nodes deleted are present in the index boolean foundNodeRef = false; - for (NodeRef.Status nodeStatus : nodeStatuses) + for (NodeRef nodeRef : nodeRefs) { - NodeRef nodeRef = nodeStatus.getNodeRef(); if (logger.isTraceEnabled()) { logger.trace( diff --git a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java index 87e426be9a..6f9be1abc6 100644 --- a/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java +++ b/source/java/org/alfresco/repo/security/authority/AuthorityDAOImpl.java @@ -20,13 +20,16 @@ package org.alfresco.repo.security.authority; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; @@ -122,8 +125,15 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor /** The number of authorities in a zone to pre-cache, allowing quick generation of 'first n' results. */ private int zoneAuthoritySampleSize = 10000; - + private NamedObjectRegistry> cannedQueryRegistry; + + private static final Collection SEARCHABLE_AUTHORITY_TYPES = new LinkedList(); + static + { + SEARCHABLE_AUTHORITY_TYPES.add(AuthorityType.ROLE); + SEARCHABLE_AUTHORITY_TYPES.add(AuthorityType.GROUP); + } public AuthorityDAOImpl() { @@ -541,19 +551,38 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor query.append("TYPE:\"").append(ContentModel.TYPE_AUTHORITY_CONTAINER).append("\""); if (displayNamePattern != null) { - query.append(" AND (@").append( - AbstractLuceneQueryParser.escape("{" + ContentModel.PROP_AUTHORITY_NAME.getNamespaceURI() + "}" - + ISO9075.encode(ContentModel.PROP_AUTHORITY_NAME.getLocalName()))).append(":\""); - // Allow for the appropriate type prefix in the authority name - if (type == null && !displayNamePattern.startsWith("*")) + query.append(" AND ("); + if (!displayNamePattern.startsWith("*")) { - query.append("*").append(AbstractLuceneQueryParser.escape(displayNamePattern)); + // Allow for the appropriate type prefix in the authority name + Collection authorityTypes = type == null ? SEARCHABLE_AUTHORITY_TYPES + : Collections.singleton(type); + boolean first = true; + for (AuthorityType subType: authorityTypes) + { + if (first) + { + first = false; + } + else + { + query.append(" OR "); + } + query.append("@").append( + AbstractLuceneQueryParser.escape("{" + ContentModel.PROP_AUTHORITY_NAME.getNamespaceURI() + "}" + + ISO9075.encode(ContentModel.PROP_AUTHORITY_NAME.getLocalName()))).append(":\""); + query.append(getName(subType, AbstractLuceneQueryParser.escape(displayNamePattern))).append("\""); + + } } else { + query.append("@").append( + AbstractLuceneQueryParser.escape("{" + ContentModel.PROP_AUTHORITY_NAME.getNamespaceURI() + "}" + + ISO9075.encode(ContentModel.PROP_AUTHORITY_NAME.getLocalName()))).append(":\""); query.append(getName(type, AbstractLuceneQueryParser.escape(displayNamePattern))); } - query.append("\" OR @").append( + query.append(" OR @").append( AbstractLuceneQueryParser.escape("{" + ContentModel.PROP_AUTHORITY_DISPLAY_NAME.getNamespaceURI() + "}" + ISO9075.encode(ContentModel.PROP_AUTHORITY_DISPLAY_NAME.getLocalName()))).append( ":\"").append(AbstractLuceneQueryParser.escape(displayNamePattern)).append("\")"); @@ -608,9 +637,8 @@ public class AuthorityDAOImpl implements AuthorityDAO, NodeServicePolicies.Befor for (ResultSetRow row : rs) { NodeRef nodeRef = row.getNodeRef(); - QName idProp = type != AuthorityType.USER - || dictionaryService.isSubClass(nodeService.getType(nodeRef), - ContentModel.TYPE_AUTHORITY_CONTAINER) ? ContentModel.PROP_AUTHORITY_NAME + QName idProp = dictionaryService.isSubClass(nodeService.getType(nodeRef), + ContentModel.TYPE_AUTHORITY_CONTAINER) ? ContentModel.PROP_AUTHORITY_NAME : ContentModel.PROP_USERNAME; addAuthorityNameIfMatches(authorities, DefaultTypeConverter.INSTANCE.convert(String.class, nodeService .getProperty(nodeRef, idProp)), type, pattern); diff --git a/source/java/org/alfresco/repo/security/authority/script/ScriptAuthorityService.java b/source/java/org/alfresco/repo/security/authority/script/ScriptAuthorityService.java index 57fd9442e1..b8c271cdf3 100644 --- a/source/java/org/alfresco/repo/security/authority/script/ScriptAuthorityService.java +++ b/source/java/org/alfresco/repo/security/authority/script/ScriptAuthorityService.java @@ -415,12 +415,9 @@ public class ScriptAuthorityService extends BaseScopableProcessorExtension { String filter = shortNameFilter; - /** - * Modify shortNameFilter to be "shortName*" - */ if (shortNameFilter.length() != 0) { - filter = filter.replace("\"", "") + "*"; + filter = filter.replace("\"", ""); } Set authorities; diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java index 2340e84bb3..9c6023bde8 100644 --- a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -415,7 +415,7 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per /** * {@inheritDoc} */ - public NodeRef getPerson(final String userName, final boolean autoCreate) + public NodeRef getPerson(final String userName, final boolean autoCreateHomeFolderAndMissingPersonIfAllowed) { // MT share - for activity service system callback if (tenantService.isEnabled() && (AuthenticationUtil.SYSTEM_USER_NAME.equals(AuthenticationUtil.getRunAsUser())) && tenantService.isTenantUser(userName)) @@ -426,17 +426,17 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per { public NodeRef doWork() throws Exception { - return getPersonImpl(userName, autoCreate); + return getPersonImpl(userName, autoCreateHomeFolderAndMissingPersonIfAllowed); } }, tenantService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenantDomain)); } else { - return getPersonImpl(userName, autoCreate); + return getPersonImpl(userName, autoCreateHomeFolderAndMissingPersonIfAllowed); } } - private NodeRef getPersonImpl(String userName, boolean autoCreate) + private NodeRef getPersonImpl(String userName, boolean autoCreateHomeFolderAndMissingPersonIfAllowed) { if(userName == null) { @@ -450,17 +450,18 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per if (personNode == null) { TxnReadState txnReadState = AlfrescoTransactionSupport.getTransactionReadState(); - if (autoCreate && createMissingPeople() && txnReadState == TxnReadState.TXN_READ_WRITE) + if (autoCreateHomeFolderAndMissingPersonIfAllowed && createMissingPeople() && + txnReadState == TxnReadState.TXN_READ_WRITE) { // We create missing people AND are in a read-write txn - return createMissingPerson(userName); + return createMissingPerson(userName, true); } else { throw new NoSuchPersonException(userName); } } - else if (autoCreate) + else if (autoCreateHomeFolderAndMissingPersonIfAllowed) { makeHomeFolderIfRequired(personNode); } @@ -749,14 +750,14 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per /** * {@inheritDoc} */ - public void setPersonProperties(String userName, Map properties, boolean autoCreate) + public void setPersonProperties(String userName, Map properties, boolean autoCreateHomeFolder) { NodeRef personNode = getPersonOrNull(userName); if (personNode == null) { if (createMissingPeople()) { - personNode = createMissingPerson(userName); + personNode = createMissingPerson(userName, autoCreateHomeFolder); } else { @@ -765,7 +766,8 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per } else { - if (autoCreate) + // Must create the home folder first as a property holds its location. + if (autoCreateHomeFolder) { makeHomeFolderIfRequired(personNode); } @@ -794,10 +796,18 @@ public class PersonServiceImpl extends TransactionListenerAdapter implements Per return true; } - private NodeRef createMissingPerson(String userName) + private NodeRef createMissingPerson(String userName, boolean autoCreateHomeFolder) { HashMap properties = getDefaultProperties(userName); NodeRef person = createPerson(properties); + + // The home folder will ONLY exist after the the person is created if + // homeFolderCreationEager == true + if (autoCreateHomeFolder && homeFolderCreationEager == false) + { + makeHomeFolderIfRequired(person); + } + return person; } diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java index 8c4f7fffcb..5752903fbe 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizer.java @@ -1420,7 +1420,7 @@ public class ChainingUserRegistrySynchronizer extends AbstractLifecycleBean impl zones); ChainingUserRegistrySynchronizer.this.authorityService.addAuthorityToZones(personName, zoneSet); ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName, - personProperties); + personProperties, false); } else { diff --git a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java index 0b521f7803..3da10f46d9 100644 --- a/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java +++ b/source/java/org/alfresco/repo/security/sync/ChainingUserRegistrySynchronizerTest.java @@ -39,10 +39,12 @@ import junit.framework.TestCase; import org.alfresco.model.ContentModel; import org.alfresco.repo.management.subsystems.ChildApplicationContextManager; import org.alfresco.repo.security.authentication.AuthenticationContext; +import org.alfresco.repo.security.person.PersonServiceImpl; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.PersonService; @@ -91,6 +93,9 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase /** The retrying transaction helper. */ private RetryingTransactionHelper retryingTransactionHelper; + + /** The value given to the person service. */ + private boolean homeFolderCreationEager; /* * (non-Javadoc) @@ -114,6 +119,7 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase this.retryingTransactionHelper = (RetryingTransactionHelper) ChainingUserRegistrySynchronizerTest.context .getBean("retryingTransactionHelper"); + setHomeFolderCreationEager(false); // the normal default if using LDAP } /* @@ -124,6 +130,7 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase protected void tearDown() throws Exception { this.authenticationContext.clearCurrentSecurityContext(); + setHomeFolderCreationEager(true); // the normal default if not using LDAP } /** @@ -231,6 +238,12 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase }, false, true); } + public void setHomeFolderCreationEager(boolean homeFolderCreationEager) + { + this.homeFolderCreationEager = homeFolderCreationEager; + ((PersonServiceImpl)personService).setHomeFolderCreationEager(homeFolderCreationEager); + } + /** * Tests a differential update of the test users and groups. The layout is as follows * @@ -438,6 +451,24 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase tearDownTestUsersAndGroups(); } + + public void testDifferentialUpdateWithHomeFolderCreation() throws Exception + { + setHomeFolderCreationEager(!homeFolderCreationEager); + testDifferentialUpdate(); + } + + public void testForcedUpdateWithHomeFolderCreation() throws Exception + { + setHomeFolderCreationEager(!homeFolderCreationEager); + testDifferentialUpdate(); + } + + public void testCaseChangeWithHomeFolderCreation() throws Exception + { + setHomeFolderCreationEager(!homeFolderCreationEager); + testDifferentialUpdate(); + } /** * Tests synchronization with a zone with a larger volume of authorities. @@ -592,6 +623,21 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase // Check case matches assertEquals(this.personService.getUserIdentifier(name), name); + + // Check the person exist. + NodeRef person = personService.getPerson(name, false); + assertNotNull("Person for "+name+" should exist", person); + + // Check the home folder exists or not. + NodeRef homeFolder = DefaultTypeConverter.INSTANCE.convert(NodeRef.class, nodeService.getProperty(person, ContentModel.PROP_HOMEFOLDER)); + if (homeFolderCreationEager) + { + assertNotNull("Home folder for "+name+" should exist", homeFolder); + } + else + { + assertNull("Home folder for "+name+" should not exist", homeFolder); + } } } @@ -626,7 +672,7 @@ public class ChainingUserRegistrySynchronizerTest extends TestCase */ private void assertEmailEquals(String personName, String email) { - NodeRef personRef = this.personService.getPerson(personName); + NodeRef personRef = this.personService.getPerson(personName, false); assertEquals(email, this.nodeService.getProperty(personRef, ContentModel.PROP_EMAIL)); } diff --git a/source/java/org/alfresco/service/cmr/model/FileFolderService.java b/source/java/org/alfresco/service/cmr/model/FileFolderService.java index 32c518adfe..cf0173e8e9 100644 --- a/source/java/org/alfresco/service/cmr/model/FileFolderService.java +++ b/source/java/org/alfresco/service/cmr/model/FileFolderService.java @@ -339,6 +339,17 @@ public interface FileFolderService @Auditable(parameters = {"rootNodeRef", "pathElements"}) public FileInfo resolveNamePath(NodeRef rootNodeRef, List pathElements) throws FileNotFoundException; + /** + * Resolve a file or folder name path from a given root node down to the final node. + * + * @param rootNodeRef the start point node - a cm:folder type or subtype, e.g. the Company Home's nodeRef + * @param pathElements a list of names in the path. Do not include the referenced rootNodeRef's path element. + * @return Returns the info of the file or folder or null if mustExist is false and the file does not exist + * @throws FileNotFoundException if no file or folder exists along the path and mustExist is true + */ + @Auditable(parameters = {"rootNodeRef", "pathElements", "mustExist"}) + public FileInfo resolveNamePath(NodeRef rootNodeRef, List pathElements, boolean mustExist) throws FileNotFoundException; + /** * Get the file info (name, folder, etc) for the given node * diff --git a/source/java/org/alfresco/service/cmr/security/PersonService.java b/source/java/org/alfresco/service/cmr/security/PersonService.java index f49e5ea91b..17a4485154 100644 --- a/source/java/org/alfresco/service/cmr/security/PersonService.java +++ b/source/java/org/alfresco/service/cmr/security/PersonService.java @@ -49,8 +49,9 @@ public interface PersonService /** * Get a person by userName. The person is store in the repository. The * person may be created as a side effect of this call, depending on the - * setting to - * {@link #setCreateMissingPeople(boolean) create missing people or not}. + * setting of + * {@link #setCreateMissingPeople(boolean) to create missing people or not}. + * The home folder will also be created as a side effect if it does not exist. * * @param userName - * the userName key to find the person @@ -66,20 +67,27 @@ public interface PersonService public NodeRef getPerson(String userName); /** - * Retrieve the person NodeRef for a username key. Depending on the autoCreate parameter and - * configuration missing people will be created if not found, else a NoSuchPersonException exception will be thrown. + * Retrieve the person NodeRef for a {@code username}, optionally creating + * the home folder if it does not exist and optionally creating the person + * if they don't exist AND the PersonService is configured to allow the + * creation of missing persons {@see #setCreateMissingPeople(boolean)}. + * + * If not allowed to create missing persons and the person does not exist + * a {@code NoSuchPersonException} exception will be thrown. * * @param userName * of the person NodeRef to retrieve - * @param autoCreate - * should we auto-create the person node and home folder if they don't exist? (and configuration allows - * us to) + * @param autoCreateHomeFolderAndMissingPersonIfAllowed + * If the person exits: + * should we create the home folder if it does not exist? + * If the person exists AND the creation of missing persons is allowed + * should we create both the person and home folder. * @return NodeRef of the person as specified by the username * @throws NoSuchPersonException * if the person doesn't exist and can't be created */ @Auditable(parameters = {"userName", "autoCreate"}) - public NodeRef getPerson(final String userName, final boolean autoCreate); + public NodeRef getPerson(final String userName, final boolean autoCreateHomeFolderAndMissingPersonIfAllowed); /** * Check if a person exists. @@ -124,7 +132,7 @@ public interface PersonService /** * Set the properties on a person - some of these may be persisted in - * different locations. + * different locations - the home folder is created if it doesn't exist * * @param userName - * the user for which the properties should be set. @@ -142,11 +150,11 @@ public interface PersonService * - the user for which the properties should be set. * @param properties * - the map of properties to set (as the NodeService) - * @param autoCreate - * should we auto-create the home folder if it doesn't exist? (and configuration allows us to) + * @param autoCreateHomeFolder + * should we auto-create the home folder if it doesn't exist. */ @Auditable(parameters = {"userName", "properties", "autoCreate"}) - public void setPersonProperties(String userName, Map properties, boolean autoCreate); + public void setPersonProperties(String userName, Map properties, boolean autoCreateHomeFolder); /** * Can this service create, delete and update person information?