mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-06-30 18:15:39 +00:00
43628: Fix for ALF-16299 - On full user profile, in the 'Sites' tab, the site descriptions are not aligned when a long site description is provided. 43639: ALF-16701: use newly released Spring Surf 1.2.0-M1 rather than a SNAPSHOT 43644: ALF-16527: webscript person.lib.ftl does not include all attributes for users out of the box. - added missing organizationId attribute. 43649: ALF-16756: WebDAV: An error occurs on drag&drop content from local machine to alfresco when inbound move rule configured. 43651: ALF-16756: Fixed typos - I took this code in good faith! 43659: ALF-16006: MT: Document Library is absent after upgrade from 3.4.x to 4.1.x (eg. 3.4.10 -> 4.1.1) - More patch dependencies required 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43675: Merged V4.1 to V4.1-BUG-FIX 43674: Merged PATCHES/V4.1.1 to V4.1 43673: Merged V3.4-BUG-FIX to PATCHES/V4.1.1 43672: ALF-16834, ALF-16833, MNT-187: Fix unit test failures and 'fine tune' logic to handle multiple updates within a single transaction creating a node 43676: ALF-12324: Can't delete site with WQS content - Don't try to add nodes to the publish queue on site deletion 43689: Fix for ALF-14207 - cm:link not correctly handled in Share's doclist when users don't have permission 43690: Fix for ALF-16538 - Wrong label on change group permission message 43696: ALF-16348 (Inconsistent error message when latest manager leaves a site) 43719: ALF-16031: "CMISChangeLog does not log Created events" 43720: ALF-14137: "When calling CMIS getAllVersions method using the OpenCMIS Browser the PreparedStatement is executed multiple times" 43722: ALF-16352: Re-instated manage permissions action to document list view 43745: Fix for ALF-15351. ContentModelFormProcessor had a TODO to handle associations defined on aspects other than those aspects already on the node for which a form is being submitted. I've added code that, when an association is added to a node, will check if the association is defined on any aspect in the system and if it is, it will let the association be created. 43757: ALF-16171: If a password for admin user is specified with a space in the end during the installation Alfresco startup fails - Added validation that will not allow the specified password to contain leading to trailing white space 43760: Fixed ALF-16317 "Labels not displaying full text when creating a rule on a folder with IE8" part 2 43769: Fix for ALF-13461. Merged PATCHES/V3.4.8 to BRANCHES/DEV/V4.1-BUG-FIX: 43344: MNT-162: Merged DEV to PATCHES/V3.4.8 43254: MNT-162: CLONE - Hidden folder "Comments" is available to searching - Removed last '/' from COMMENT_QNAMEPATH constant. 43770: ALF-15616: Merged V3.4-BUG-FIX (3.4.12) to V4.1-BUG-FIX (4.1.3) 43768: Merged Dev to V3.4-BUG-FIX (3.4.12) 43743: ALF-11956: WCM accessibility The problem with absence of initial focus on the first Web form element has been resolved in 'alfresco.xforms.XForm._loadHandler()'. This method creates all controls, defined in the form. 'alfresco.xforms.FocusResolver._findControl()' has been renamed to a public-like method ('alfresco.xforms.FocusResolver.findControl()'). Also it has been modified to introduce a possibility of searching elements, using root XForms widgets container. Some other minor fixes. 43772: Fixed ALF-16497 "Selected Items drop down box is expanded to the right in IE9" 43787: Merged in Philippe's fix for ALF-16313:unmakeTranslation() called on pivot language gives exception 43794: ALF-16155: "Notification digest accumulates and is repeatedly sent if 1 or more notification email fails" 43805: ALF-16212: Fixes double encoding issue. 43835: Fixed ALF-13595: Patches third party library date.js to prevent infinite loop. 43836: ALF-16480: Merged PATCHES/V4.1.1 to V4.1-BUG-FIX 43252: MNT-166: Document lock not removed after the lock expiry date has been reached - Changed evaluator "evaluator.doclib.metadata.isLocked" to use "evaluator.doclib.indicator.nodeLocked" and "evaluator.doclib.indicator.lockOwner" evaluator. 43253: MNT-165: "Cancel Editing" does not completely remove lock from document - Added documentLibrary cancel editing action for locked documents. 43300: MNT-171: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 (modified) 43096: Fix for ALF-16283 - When document is checked out, 'Edit Online' and 'Upload New Version' options should not be visible on the original document. 43311: MNT-165: "Cancel Editing" does not completely remove lock from document - Fix for page refresh problem when cancel editing on details page 43421: MNT-186: 4.1.1.7 HF: Webscipt error on doclib page, containing locked by other users files - Change evaluator.doclib.metadata.isLocked to break circular dependency 43755: MNT-202: Upload New Version not available for a document that has been edited offline - Upload New Version is now available if editable by user (respecting locks, type of checkout, etc). 43844: ALF-16696: Merged DEV to V4.1-BUG-FIX 43734: Share nodebrowser is unable to access node of a document with MS residual properties - Use localname if no prefix is registered for a namespace uri 43864: Fixed ALF-16320 "Properties side panel collapses after editing properties in document preview window with IE8" 43866: Fixed ALF-16320 "Properties side panel collapses after editing properties in document preview window with IE8" part 2 - Checking using YAHOO.util.Event.getTarget instead 43867: Fixed ALF-16276 "'"Web View' dahslet displays 2 scrollbars in IE8." 43872: Merged V4.1 to V4.1-BUG-FIX 43622: ALF-16757: Sharepoint doesn't work correct with SSO - Fix by Pavel 43633: Latest translations from Gloria (r43623) 43636: Merged PATCHES/V4.1.1 to V4.1 43301: ALF-16811 / MNT-173: SOLR tracking spending too long evaluating paths - Too much time was being spent fetching the individual nodes in each path and there could be potentially thousands - Now we traverse all the ancestor parent associations in the cache before switching shared cache reads off, then bulk load them in one shot! 43303: ALF-16812 / MNT-174: "dictionaryDAO.init() called, yet no namespace registry for domain" after node rejoins cluster - namespaceRegistryCache is secondary to the clustered dictionaryRegistryCache, so can be a non-clustered cache. Synchronization and thread locals already in use so still thread safe. - dictionaryDAO.init() now has sole responsibility of establishing the NamespaceRegistry threadlocal within its locks, so no more risk of cyclic dependencies, race conditions, partial initialization, or registries dropping out of the cache - To avoid being confused by the 'consistent read' behaviour of a transactional cache, DictionaryDAOImpl now reads / writes directly to a shared cache. Again locks and thread locals still used so still thread safe 43334: ALF-16812 / MNT-174: Fixed failing unit tests - Because DictionaryDAOImpl now reads directly from the shared cache we need to reset it on initialization to avoid problems in multi-context unit tests 43337: ALF-16811 / MNT-173: Fix test failure. Cope with IDs of deleted nodes in getCachedAncestors() 43356: ALF-16811 / MNT-173: If we are disabling shared cache reads for the transaction, convert all existing reads and updates to avoid 'consistent read' behaviour giving us a potentially out of date node already accessed - Existing read buckets are simply thrown away - Updates are converted to removes to avoid any assumptions about existing shared cache content - New entries are left alone as they haven't come from the shared cache anyway 43410: ALF-16813 / MNT-185: Web Scripts are being endlessly re-registering in clustered environment - Due to 'consistent read' behaviour of transactional cache - As accesses are regulated by RW locks we can read straight through to the shared cache instead 43565: ALF-16814 / MNT-190: Bmlab Solr Node 2 threw unhandled NullPointerException (and possibly made solr unresponsive) - Added missing return statement 43646: ALF-15755: Rationalization of WebDAVMethod.checkNode() 43681: Merged HEAD to V4.1 43656: Fix for ALF-16683 CMIS: cannot navigate to original document was created through CMIS with CHECKED OUT version state. 43698: Latest Russian translations from Gloria 43838: ALF-16875: Merged V4.1-BUG-FIX to V4.1 43836: ALF-16480: Merged PATCHES/V4.1.1 to V4.1-BUG-FIX 43252: MNT-166: Document lock not removed after the lock expiry date has been reached - Changed evaluator "evaluator.doclib.metadata.isLocked" to use "evaluator.doclib.indicator.nodeLocked" and "evaluator.doclib.indicator.lockOwner" evaluator. 43253: MNT-165: "Cancel Editing" does not completely remove lock from document - Added documentLibrary cancel editing action for locked documents. 43300: MNT-171: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 (modified) 43096: Fix for ALF-16283 - When document is checked out, 'Edit Online' and 'Upload New Version' options should not be visible on the original document. 43311: MNT-165: "Cancel Editing" does not completely remove lock from document - Fix for page refresh problem when cancel editing on details page 43421: MNT-186: 4.1.1.7 HF: Webscipt error on doclib page, containing locked by other users files - Change evaluator.doclib.metadata.isLocked to break circular dependency 43755: ALF-16890 / MNT-202: Upload New Version not available for a document that has been edited offline - Upload New Version is now available if editable by user (respecting locks, type of checkout, etc). 43868: Reverse merging r43838 - Merge in wrong direction introducing duplicate fragment into share-documentlibrary-config.xml 43871: ALF-16890: Merged PATCHES/V4.1.1 to V4.1 43755: MNT-202: Upload New Version not available for a document that has been edited offline - Upload New Version is now available if editable by user (respecting locks, type of checkout, etc). 43873: Merged V4.1 to V4.1-BUG-FIX (RECORD ONLY) 43602: ALF-16254: Merged V4.1-BUG-FIX to V4.1 43598: Merged HEAD to BRANCHES/DEV/V4.1-BUG-FIX 41906: ALF-11378: REST API has been modified to return extra information about a user whether s/he belongs to a group or not. 43612: ALF-16598: Merged V4.1-BUG-FIX to V4.1 43252: MNT-166: Document lock not removed after the lock expiry date has been reached - Changed evaluator "evaluator.doclib.metadata.isLocked" to use "evaluator.doclib.indicator.nodeLocked" and "evaluator.doclib.indicator.lockOwner" evaluator. 43254: MNT-165: "Cancel Editing" does not completely remove lock from document - Added documentLibrary cancel editing action for locked documents. 43300: MNT-171: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 (modified) 43096: Fix for ALF-16283 - When document is checked out, 'Edit Online' and 'Upload New Version' options should not be visible on the original document. 43311: MNT-165: "Cancel Editing" does not completely remove lock from document - Fix for page refresh problem when cancel editing on details page 43421: MNT-186: 4.1.1.7 HF: Webscipt error on doclib page, containing locked by other users files - Change evaluator.doclib.metadata.isLocked to break circular dependency 43615: ALF-16794: Merged V4.1-BUG-FIX to V4.1 43478: MNT-181: Now WebDAV will ALWAYS preserve the original metadata and versions of ANY node that is temporarily 'moved out' in ANY kind of 'shuffle' operation - To make the source node temporarily invisible to WebDAV the client specific HIDDEN aspect features are used - WebDAVHelper.isRenameShuffle() method introduced, to parallel ALF-3856 CIFS fix and using similar system.webdav.renameShufflePattern global property to detect the start of a shuffle - WebDAVHelper converted to use proper dependency injection - CopyMethod has become a simple subclass of MoveMethod as all the hidden aspect munging is done by it - DeleteMethod now preserves hidden nodes - PropFindMethod now ignores hidden nodes - Listing methods will hide hidden nodes from WebDAV 43483: MNT-181: Corrected typo 43523: MNT-181: Corrections - WebDAVLockService.unlock() made 'harmless' to call on already-unlocked nodes - Delete method hides rather than deletes versioned nodes and working copes in case it is called by OSX Finder during a 'replace' operation 43524: MNT-181: Correction - PutMethod now 'unhides' hidden nodes and behaves as though it created them 43570: MNT-181: More corrections researched by Valery - Don't treat all moves to temporary locations as copies - just those from non-temporary locations. Avoids initial upload leaving lots of hidden files around. - Only copy the content, not the whole node including aspects to avoid versioning temporary files! - Don't version on changes to sys:clientVisibilityMask - avoids 'double versioning' - Recognize Mac .TemporaryItems folder and ._ files as temporary 43586: MNT-181: Final correction researched by Valery - Corrected system.webdav.renameShufflePattern so that it matches .TemporaryItems folder and ._ files as a full match 43616: ALF-15755: Merged V4.1-BUG-FIX to V4.1 43591: ALF-16772: If the WebDAV path of a document exceeds 255 characters, documents opened in MSOffice cannot be saved back - Interpret null nodeLockToken as not locked. 43629: Merged V4.1-BUG-FIX to V4.1 (4.1.2) 43498: Fix for ALF-16648 - Alfresco Enterprise artifacts in artifacts.alfresco.com do not provide POM files / dependencies declarations: Merged HEAD to V4.1-BUG-FIX (4.1.2) 43380: -- added site content for alfresco-platform-distribution POM 43379: -- added site documentation for alfresco-platform-distribution POM 43378: -- added site documentation for alfresco-platform-distribution POM -- deployed site for 4.2.b Community at https://artifacts.alfresco.com/nexus/content/repositories/alfresco-docs/alfresco-platform-distribution/latest/index.html -- created repository for Enterprise docs and added url in the appropriate edition properties 43273: Use property to define POI version 42966: ALF-14353 - Added platform distribution POM to standard maven-deploy procedure 42965: ALF-14353 - added alfresco-platform-distribution to provide a Maven release descriptor (dependencyManagement) per each Community / Enterprise release -- moved maven-ant-tasks not to be in the runtime lib -- added platform distribution pom in the SDK folder -- updated maven.xml to deploy filter and deploy the appropriate platform-distribution POM per each releae -- in maven.xml moved configure-release and configure-snapshot goals to maven-env-prerequisites -- updated sdk readme to explain the presence of alfresco-platform-distribution POM 42912: -- updated README header on the POM specifying it's NOT usable to build Alfresco -- make a clear reference to the POMs that get deployed by pom-experimental.xml being usable for development 42842: ALF-14353: Fix artifactId alfresco-jlan -> alfresco-jlan-embed 41883: ALF-14353 - fixed multiple Maven build issues. Now mvn clean install -f pom-experimental.xml works fine. Also deployed Spring Surf 1.2.0-SNAPSHOT so proper Surf version is retrieved 41882: added pre-requisites to build POMs successfully with mvn clean install -f pom-experimental.xml 43634: Merged V4.1-BUG-FIX to V4.1 43386: ALF-13091: Prevent bean post processor propagation to child application contexts. Remove Jsr250BeanPostPorcessor from the CXF configuration, to prevent strange interaction with component scanning. 43641: Merged V4.1-BUG-FIX to V4.1 (4.1.2) 43639: ALF-16701: use newly released Spring Surf 1.2.0-M1 in POM files rather than a SNAPSHOT 43645: Merged V4.1-BUG-FIX to V4.1 43644: ALF-16527: webscript person.lib.ftl does not include all attributes for users out of the box. - added missing organizationId attribute. 43660: Merged V4.1-BUG-FIX to V4.1 43659: ALF-16006: MT: Document Library is absent after upgrade from 3.4.x to 4.1.x (eg. 3.4.10 -> 4.1.1) - More patch dependencies required 43669: Merged V4.1-BUG-FIX to V4.1 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43697: Merged V4.1-BUG-FIX to V4.1 43689: Fix for ALF-14207 - cm:link not correctly handled in Share's doclist when users don't have permission 43761: Merged V4.1-BUG-FIX to V4.1 43760: Fixed ALF-16317 "Labels not displaying full text when creating a rule on a folder with IE8" part 2 43796: Merged V4.1-BUG-FIX to V4.1 43795: Fix for ALF-16254 - "Leave Site" behaviour for group based site membership: Blah, Blah, Blah! 43883: Fix for ALF-12711. Separated preparation of email message from sending of email message as described in analysis in JIRA. Did not merge the fix provided as MailActionExecuter.java.diff but reimplemented a fix based on that. 43888: ALF-16781: Merged V3.4-BUG-FIX (3.4.12) to V4.1-BUG-FIX (4.1.3) 43887: ALF-16898 CLONE - Transformation Server history shows incorrect transformation "To" type for images - ImageTransformActionExecuter ("Transform and Copy Image") re-factored to use ContentService rather than hard coded to ImageMagick 43900: Merged V4.1 to V4.1-BUG-FIX 43898: Merged PATCHES/V4.1.1 to V4.1 43708: ALF-16903 / MNT-203: ACL changes not propagated between two non clustered Alfresco instances hitting same Solr and Database - Although SOLRTrackingComponentImpl was not trusting the cache when tracking node transactions, it was still trusting the cache for ACL change sets - This mean that when the SOLR node is using an Alfresco out of the cluster (which we are supposed to support) it could see stale ACLs - Fixed this by using the same strategy we use in AbstractNodeDAO - Now the main acl CRUD cache is set to ignore the shared cache during SOLR tracking - All secondary ACL caches are keyed by ACL ID AND version, so it prevents the possibility of retrieving a stale cached ACL for an old version 43713: ALF-16903 / MNT-203: ACL changes not propagated between two non clustered Alfresco instances hitting same Solr and Database - Fix test failures - deleteAccessControlEntries must 'touch' all the ACLs it affects to keep caches valid - createAccessControlList should return the newly-versioned ACL 43736: ALF-16904 / MNT-204: Index showing no progress on Solr server - SAP's tracking thread got stuck indefinitely waiting on a content response after sending a request, probably due to some misbehaving proxy or balancer - Now we make it possible to recover from this - A new alfresco.socketTimeout parameter is now supported in solrcore.properties - It specifies the number of milliseconds SOLR will wait before giving up waiting for data on an HTTP connection - The default is still zero which means wait indefinitely 43759: ALF-16904 / MNT-204: Fixed compilation error 43899: Merged PATCHES/V4.1.1 to V4.1 (RECORD ONLY) 43667: Merged V4.1-BUG-FIX to PATCHES/V4.1.1 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43901: Merged V3.4-BUG-FIX to V4.1-BUG-FIX 43571: Merged DEV to V3.4-BUG-FIX 43569: ALF-16222: It's impossible to delete a file/message via IMAP using Microsoft Entourage 2008 in MacOSX 10.8 fixed: UID failed.Existing file or folder error on attempt to delete file if deleted items already contains file with the same name. modified: AttachmentExtractor modified to avoid code dublication. 43599: Fix for ALF-16505 - Discussion topics are sorted in ascending order (oldest first and newest last) when selecting 'All topics' 43610: Merged Dev to V3.4-BUGFIX (3.4.12) 43416: ALF-16470: SPP:Meeting recurent events are displayed incorrect in share calendar in all tabs (Day, Week, Month) Incorrect lucene query to search for events (it was search for events ONLY after fromDate, that gets from browser query. And recursive events, that have startDate before browser's query date, wasn't included into summary query for search for events. Start date is incorrect and is sets two times. Interval for month of end date is to small. Recursive event, that was started in previous month, and ends in current month, isn't included into result of search. For events, that ends on next date after start date, displays (view by month) only start date. 43625: ALF-11817: Cope with incomplete lock token headers from Microsoft-WebDAV-MiniRedir without the enclosing <> by just consuming the whole string 43670: ALF-11817: Prevent auto-hidden dot underscore files from reappearing on a put by checking for a shuffle path before 'unhiding' 43746: Merged DEV to V3.4-BUG-FIX (with corrections) 43692: ALF-16808 Webdav: Two versions of document have been added after the document has been rewritten once more via drag and drop action. 1. Checked whether the current content property is empty. 2. Disabled the versionable aspect. 3. Added the new content to the node. 43763: Fix for ALF-14828 - Incorrect behavior on delete action (WCMQS site) 43771: Fix for ALF-12752 - Custom form appearance parameters not accounted for causing ovverlapped textareas xforms changing their height 43773: Merged DEV to V3.4-BUG-FIX (3.4.12) 42010: ALF-14040: Event start/end time displays incorrect on MS Outlook Calendar and Calendar of created Meeting workspace 1) Send a date for "Site Calendar" dashlet in ISO-8601 format (like in v4.1.1), then after transformation to client's time zone it is displayed correct 2) For "My Calendar" dashlet we need to take into account that if the event is "all day event", then date should be used without time zone transformation 43804: ALF-12326 HomeFolderProviderSynchronizer fails to move any user space which has a rule configured on it 43837: Fixes ALF-12145: Date.js patches merged to 3.4 Merged BRANCHES/DEV/V4.0-BUG-FIX to BRANCHES/DEV/V3.4-BUG-FIX: 36202: ALF-13483: Japanese: Incorrect date handle in a date Input filed Merged BRANCHES/DEV/V4.1-BUG-FIX to BRANCHES/DEV/V3.4-BUG-FIX: 43835: Fixed ALF-13595: Patches third party library date.js to prevent infinite loop. 43839: ALF-16869: Merged PATCHES/V3.4.8 to V3.4-BUG-FIX 43344: MNT-162: Merged DEV to PATCHES/V3.4.8 43254: MNT-162: CLONE - Hidden folder "Comments" is available to searching - Removed last '/' from COMMENT_QNAMEPATH constant. 43902: Merged V3.4-BUG-FIX to V4.1-BUG-FIX (RECORD ONLY) 43177: Merged DEV to V3.4-BUG-FIX 43087: ALF-16474: Records Management groups were not deleted after removing RM site - Delete Records Management groups on ASPECT_RECORDS_MANAGEMENT_ROOT delete - Backport of RM-190 from RM 2.0 43228: ALF-16266: Merged HEAD to V3.4-BUG-FIX 32846: Fixes: ALF-10519: Issues with translation of roles. 31413: Fixes: ALF-10519 - Internationalises the role names for the repo browser's manage permissions page & makes these available to other pages through common.properties 43229: ALF-16266: Fix conflict data left in the properties file. 43624: ALF-11817: Merged PATCHES/V4.0.2 to V3.4-BUG-FIX 43587: Merged BRANCHES/V4.1-BUG-FIX to PATCHES/V4.0.2 42363: ALF-16213: renaming versioned file results in file being deleted. 43478: MNT-181: Now WebDAV will ALWAYS preserve the original metadata and versions of ANY node that is temporarily 'moved out' in ANY kind of 'shuffle' operation - To make the source node temporarily invisible to WebDAV the client specific HIDDEN aspect features are used - WebDAVHelper.isRenameShuffle() method introduced, to parallel ALF-3856 CIFS fix and using similar system.webdav.renameShufflePattern global property to detect the start of a shuffle - WebDAVHelper converted to use proper dependency injection - CopyMethod has become a simple subclass of MoveMethod as all the hidden aspect munging is done by it - DeleteMethod now preserves hidden nodes - PropFindMethod now ignores hidden nodes - Listing methods will hide hidden nodes from WebDAV 43483: MNT-181: Corrected typo 43523: MNT-181: Corrections - WebDAVLockService.unlock() made 'harmless' to call on already-unlocked nodes - Delete method hides rather than deletes versioned nodes and working copies in case it is called by OSX Finder during a 'replace' operation 43524: MNT-181: Correction - PutMethod now 'unhides' hidden nodes and behaves as though it created them 43570: MNT-181: More corrections researched by Valery - Don't treat all moves to temporary locations as copies - just those from non-temporary locations. Avoids initial upload leaving lots of hidden files around. - Only copy the content, not the whole node including aspects to avoid versioning temporary files! - Don't version on changes to sys:clientVisibilityMask - avoids 'double versioning' - Recognize Mac .TemporaryItems folder and ._ files as temporary 43586: MNT-181: Final correction researched by Valery - Corrected system.webdav.renameShufflePattern so that it matches .TemporaryItems folder and ._ files as a full match 43671: ALF-16834: Merged V4.1-BUG-FIX to V3.4-BUG-FIX 43666: ALF-16833 / MNT-187: Fixed regression in inbound rule firing caused by the fix to ALF-14744 - Inbound rules were not firing for newly created content nodes that had null content properties (e.g. dataListItems) - Now the ALF-14744 fix has been revised so that inbound rules are only not fired for new nodes with zero-length content, as possibly created by the OSX / Windows 7 WebDAV clients during an upload, and only if policy.content.update.ignoreEmpty=true (the default) - OnContentUpdateRuleTrigger removed. Now all content property updates are handled by OnPropertyUpdateRuleTrigger so that we can have this subtle treatment of 'empty' content properties. - Reverted ALF-14744 changes to RuleServiceCoverageTest and added new tests for 'empty' content and ASPECT_NO_CONTENT - Updated RuleTriggerTest in line with changes 43842: Merged V4.0-BUG-FIX to V3.4-BUG-FIX 33387: ALF-12492 - Email with empty subject sent to Alfresco by SMTP cause Null pointer Exception 43843: ALF-16717: Merged V4.1-BUG-FIX to V3.4-BUG-FIX 43314: ALF-16575 - Email server does not accept email where Subject ends with a period git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@43914 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2164 lines
87 KiB
Java
2164 lines
87 KiB
Java
/*
|
|
* Copyright (C) 2005-2011 Alfresco Software Limited.
|
|
*
|
|
* This file is part of Alfresco
|
|
*
|
|
* Alfresco is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Alfresco is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.alfresco.repo.imap;
|
|
|
|
import static org.alfresco.repo.imap.AlfrescoImapConst.DICTIONARY_TEMPLATE_PREFIX;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.io.Serializable;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.NavigableMap;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
|
|
import javax.mail.Flags;
|
|
import javax.mail.Flags.Flag;
|
|
import javax.mail.MessagingException;
|
|
import javax.mail.Multipart;
|
|
import javax.mail.Part;
|
|
import javax.mail.internet.AddressException;
|
|
import javax.mail.internet.ContentType;
|
|
import javax.mail.internet.InternetAddress;
|
|
import javax.mail.internet.MimeMessage;
|
|
import javax.mail.internet.MimeUtility;
|
|
|
|
import org.alfresco.error.AlfrescoRuntimeException;
|
|
import org.alfresco.model.ContentModel;
|
|
import org.alfresco.model.ImapModel;
|
|
import org.alfresco.repo.admin.SysAdminParams;
|
|
import org.alfresco.repo.cache.SimpleCache;
|
|
import org.alfresco.repo.imap.AlfrescoImapConst.ImapViewMode;
|
|
import org.alfresco.repo.imap.config.ImapConfigMountPointsBean;
|
|
import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy;
|
|
import org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy;
|
|
import org.alfresco.repo.node.NodeServicePolicies.OnDeleteChildAssociationPolicy;
|
|
import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy;
|
|
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
|
|
import org.alfresco.repo.policy.BehaviourFilter;
|
|
import org.alfresco.repo.policy.JavaBehaviour;
|
|
import org.alfresco.repo.policy.PolicyComponent;
|
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
|
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
|
|
import org.alfresco.repo.security.permissions.AccessDeniedException;
|
|
import org.alfresco.repo.site.SiteModel;
|
|
import org.alfresco.repo.site.SiteServiceException;
|
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
|
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
|
|
import org.alfresco.repo.transaction.TransactionListenerAdapter;
|
|
import org.alfresco.service.ServiceRegistry;
|
|
import org.alfresco.service.cmr.lock.NodeLockedException;
|
|
import org.alfresco.service.cmr.model.FileExistsException;
|
|
import org.alfresco.service.cmr.model.FileFolderService;
|
|
import org.alfresco.service.cmr.model.FileFolderUtil;
|
|
import org.alfresco.service.cmr.model.FileInfo;
|
|
import org.alfresco.service.cmr.model.FileNotFoundException;
|
|
import org.alfresco.service.cmr.model.SubFolderFilter;
|
|
import org.alfresco.service.cmr.preference.PreferenceService;
|
|
import org.alfresco.service.cmr.repository.AssociationRef;
|
|
import org.alfresco.service.cmr.repository.ChildAssociationRef;
|
|
import org.alfresco.service.cmr.repository.ContentData;
|
|
import org.alfresco.service.cmr.repository.ContentWriter;
|
|
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
|
|
import org.alfresco.service.cmr.repository.MimetypeService;
|
|
import org.alfresco.service.cmr.repository.NodeRef;
|
|
import org.alfresco.service.cmr.repository.NodeService;
|
|
import org.alfresco.service.cmr.repository.StoreRef;
|
|
import org.alfresco.service.cmr.search.SearchService;
|
|
import org.alfresco.service.cmr.security.AccessStatus;
|
|
import org.alfresco.service.cmr.security.PermissionService;
|
|
import org.alfresco.service.cmr.security.PersonService;
|
|
import org.alfresco.service.cmr.site.SiteInfo;
|
|
import org.alfresco.service.namespace.NamespaceService;
|
|
import org.alfresco.service.namespace.QName;
|
|
import org.alfresco.util.EqualsHelper;
|
|
import org.alfresco.util.FileFilterMode;
|
|
import org.alfresco.util.FileFilterMode.Client;
|
|
import org.alfresco.util.GUID;
|
|
import org.alfresco.util.MaxSizeMap;
|
|
import org.alfresco.util.Pair;
|
|
import org.alfresco.util.PropertyCheck;
|
|
import org.alfresco.util.config.RepositoryFolderConfigBean;
|
|
import org.apache.commons.logging.Log;
|
|
import org.apache.commons.logging.LogFactory;
|
|
import org.apache.poi.hmef.HMEFMessage;
|
|
import org.springframework.context.ApplicationEvent;
|
|
import org.springframework.extensions.surf.util.AbstractLifecycleBean;
|
|
import org.springframework.extensions.surf.util.I18NUtil;
|
|
import org.springframework.util.FileCopyUtils;
|
|
|
|
import com.icegreen.greenmail.store.SimpleStoredMessage;
|
|
|
|
/**
|
|
* @author Dmitry Vaserin
|
|
* @author Arseny Kovalchuk
|
|
* @author David Ward
|
|
* @since 3.2
|
|
*/
|
|
public class ImapServiceImpl implements ImapService, OnCreateChildAssociationPolicy, OnDeleteChildAssociationPolicy, OnUpdatePropertiesPolicy, BeforeDeleteNodePolicy
|
|
{
|
|
private Log logger = LogFactory.getLog(ImapServiceImpl.class);
|
|
|
|
private static final String ERROR_FOLDER_ALREADY_EXISTS = "imap.server.error.folder_already_exist";
|
|
private static final String ERROR_MAILBOX_NAME_IS_MANDATORY = "imap.server.error.mailbox_name_is_mandatory";
|
|
private static final String ERROR_CANNOT_GET_A_FOLDER = "imap.server.error.cannot_get_a_folder";
|
|
private static final String ERROR_CANNOT_PARSE_DEFAULT_EMAIL = "imap.server.error.cannot_parse_default_email";
|
|
|
|
private static final String CHECKED_NODES = "imap.flaggable.aspect.checked.list";
|
|
private static final String FAVORITE_SITES = "imap.favorite.sites.list";
|
|
private static final String UIDVALIDITY_TRANSACTION_LISTENER = "imap.uidvalidity.txn.listener";
|
|
|
|
private SysAdminParams sysAdminParams;
|
|
private FileFolderService fileFolderService;
|
|
private NodeService nodeService;
|
|
private PermissionService permissionService;
|
|
private ServiceRegistry serviceRegistry;
|
|
private BehaviourFilter policyBehaviourFilter;
|
|
private MimetypeService mimetypeService;
|
|
private NamespaceService namespaceService;
|
|
private SearchService searchService;
|
|
private AttachmentsExtractor attachmentsExtractor;
|
|
|
|
// Note that this cache need not be cluster synchronized, as it is keyed by the cluster-safe
|
|
// change token. Key is username, changeToken
|
|
private Map<Pair<String, String>, FolderStatus> folderCache;
|
|
private int folderCacheSize = 1000;
|
|
private ReentrantReadWriteLock folderCacheLock = new ReentrantReadWriteLock();
|
|
private SimpleCache<NodeRef, CacheItem> messageCache;
|
|
private Map<String, ImapConfigMountPointsBean> imapConfigMountPoints;
|
|
private Map<String, Integer> mountPointIds;
|
|
private RepositoryFolderConfigBean[] ignoreExtractionFoldersBeans;
|
|
private RepositoryFolderConfigBean imapHomeConfigBean;
|
|
|
|
private NodeRef imapHomeNodeRef;
|
|
private Set<NodeRef> ignoreExtractionFolders;
|
|
|
|
private String defaultFromAddress;
|
|
private String defaultToAddress;
|
|
private String repositoryTemplatePath;
|
|
private boolean extractAttachmentsEnabled = true;
|
|
|
|
private Map<EmailBodyFormat, String> defaultBodyTemplates;
|
|
|
|
private final static Map<QName, Flags.Flag> qNameToFlag;
|
|
private final static Map<Flags.Flag, QName> flagToQname;
|
|
|
|
private boolean imapServerEnabled = false;
|
|
|
|
static
|
|
{
|
|
qNameToFlag = new HashMap<QName, Flags.Flag>();
|
|
qNameToFlag.put(ImapModel.PROP_FLAG_ANSWERED, Flags.Flag.ANSWERED);
|
|
qNameToFlag.put(ImapModel.PROP_FLAG_DELETED, Flags.Flag.DELETED);
|
|
qNameToFlag.put(ImapModel.PROP_FLAG_DRAFT, Flags.Flag.DRAFT);
|
|
qNameToFlag.put(ImapModel.PROP_FLAG_SEEN, Flags.Flag.SEEN);
|
|
qNameToFlag.put(ImapModel.PROP_FLAG_RECENT, Flags.Flag.RECENT);
|
|
qNameToFlag.put(ImapModel.PROP_FLAG_FLAGGED, Flags.Flag.FLAGGED);
|
|
|
|
flagToQname = new HashMap<Flags.Flag, QName>();
|
|
flagToQname.put(Flags.Flag.ANSWERED, ImapModel.PROP_FLAG_ANSWERED);
|
|
flagToQname.put(Flags.Flag.DELETED, ImapModel.PROP_FLAG_DELETED);
|
|
flagToQname.put(Flags.Flag.DRAFT, ImapModel.PROP_FLAG_DRAFT);
|
|
flagToQname.put(Flags.Flag.SEEN, ImapModel.PROP_FLAG_SEEN);
|
|
flagToQname.put(Flags.Flag.RECENT, ImapModel.PROP_FLAG_RECENT);
|
|
flagToQname.put(Flags.Flag.FLAGGED, ImapModel.PROP_FLAG_FLAGGED);
|
|
}
|
|
|
|
/**
|
|
* Bootstrap initialization bean for the service implementation.
|
|
*
|
|
* @author Derek Hulley
|
|
* @since 3.2
|
|
*/
|
|
public static class ImapServiceBootstrap extends AbstractLifecycleBean
|
|
{
|
|
private ImapServiceImpl service;
|
|
|
|
public void setService(ImapServiceImpl service)
|
|
{
|
|
this.service = service;
|
|
}
|
|
|
|
@Override
|
|
protected void onBootstrap(ApplicationEvent event)
|
|
{
|
|
service.startupInTxn(false);
|
|
}
|
|
|
|
@Override
|
|
protected void onShutdown(ApplicationEvent event)
|
|
{
|
|
AuthenticationUtil.runAs(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
if (service.getImapServerEnabled())
|
|
{
|
|
service.shutdown();
|
|
}
|
|
return null;
|
|
}
|
|
}, AuthenticationUtil.getSystemUserName());
|
|
}
|
|
}
|
|
|
|
public void setSysAdminParams(SysAdminParams sysAdminParams)
|
|
{
|
|
this.sysAdminParams = sysAdminParams;
|
|
}
|
|
|
|
public void setMessageCache(SimpleCache<NodeRef, CacheItem> messageCache)
|
|
{
|
|
this.messageCache = messageCache;
|
|
}
|
|
|
|
public void setFileFolderService(FileFolderService fileFolderService)
|
|
{
|
|
this.fileFolderService = fileFolderService;
|
|
}
|
|
|
|
public void setMimetypeService(MimetypeService mimetypeService)
|
|
{
|
|
this.mimetypeService = mimetypeService;
|
|
}
|
|
|
|
public void setNodeService(NodeService nodeService)
|
|
{
|
|
this.nodeService = nodeService;
|
|
}
|
|
|
|
public void setPermissionService(PermissionService permissionService)
|
|
{
|
|
this.permissionService = permissionService;
|
|
}
|
|
|
|
public void setServiceRegistry(ServiceRegistry serviceRegistry)
|
|
{
|
|
this.serviceRegistry = serviceRegistry;
|
|
}
|
|
|
|
public void setPolicyFilter(BehaviourFilter policyFilter)
|
|
{
|
|
this.policyBehaviourFilter = policyFilter;
|
|
}
|
|
|
|
|
|
public void setAttachmentsExtractor(AttachmentsExtractor attachmentsExtractor)
|
|
{
|
|
this.attachmentsExtractor = attachmentsExtractor;
|
|
}
|
|
|
|
public void setImapHome(RepositoryFolderConfigBean imapHomeConfigBean)
|
|
{
|
|
this.imapHomeConfigBean = imapHomeConfigBean;
|
|
}
|
|
|
|
public void setFolderCacheSize(int folderCacheSize)
|
|
{
|
|
this.folderCacheSize = folderCacheSize;
|
|
}
|
|
|
|
public String getDefaultFromAddress()
|
|
{
|
|
return defaultFromAddress;
|
|
}
|
|
|
|
public void setDefaultFromAddress(String defaultFromAddress)
|
|
{
|
|
this.defaultFromAddress = defaultFromAddress;
|
|
}
|
|
|
|
public String getDefaultToAddress()
|
|
{
|
|
return defaultToAddress;
|
|
}
|
|
|
|
public void setDefaultToAddress(String defaultToAddress)
|
|
{
|
|
this.defaultToAddress = defaultToAddress;
|
|
}
|
|
|
|
public String getWebApplicationContextUrl()
|
|
{
|
|
return sysAdminParams.getAlfrescoProtocol() + "://" + sysAdminParams.getAlfrescoHost() + ":" + sysAdminParams.getAlfrescoPort() + "/" + sysAdminParams.getAlfrescoContext();
|
|
}
|
|
|
|
public String getShareApplicationContextUrl()
|
|
{
|
|
return sysAdminParams.getShareProtocol() + "://" + sysAdminParams.getShareHost() + ":" + sysAdminParams.getSharePort() + "/" + sysAdminParams.getShareContext();
|
|
}
|
|
|
|
public String getRepositoryTemplatePath()
|
|
{
|
|
return repositoryTemplatePath;
|
|
}
|
|
|
|
public void setRepositoryTemplatePath(String repositoryTemplatePath)
|
|
{
|
|
this.repositoryTemplatePath = repositoryTemplatePath;
|
|
}
|
|
|
|
public void setImapConfigMountPoints(ImapConfigMountPointsBean[] imapConfigMountPointsBeans)
|
|
{
|
|
this.imapConfigMountPoints = new LinkedHashMap<String, ImapConfigMountPointsBean>(
|
|
imapConfigMountPointsBeans.length * 2);
|
|
this.mountPointIds = new HashMap<String, Integer>(imapConfigMountPointsBeans.length * 2);
|
|
for (int i = 0; i < imapConfigMountPointsBeans.length; i++)
|
|
{
|
|
String name = imapConfigMountPointsBeans[i].getMountPointName();
|
|
this.imapConfigMountPoints.put(name, imapConfigMountPointsBeans[i]);
|
|
this.mountPointIds.put(name, i + 1);
|
|
}
|
|
}
|
|
|
|
public void setIgnoreExtractionFolders(final RepositoryFolderConfigBean[] ignoreExtractionFolders)
|
|
{
|
|
this.ignoreExtractionFoldersBeans = ignoreExtractionFolders;
|
|
}
|
|
|
|
public void setExtractAttachmentsEnabled(boolean extractAttachmentsEnabled)
|
|
{
|
|
this.extractAttachmentsEnabled = extractAttachmentsEnabled;
|
|
}
|
|
|
|
public void setImapServerEnabled(boolean enabled)
|
|
{
|
|
this.imapServerEnabled = enabled;
|
|
}
|
|
|
|
public boolean getImapServerEnabled()
|
|
{
|
|
return this.imapServerEnabled;
|
|
}
|
|
|
|
// ---------------------- Lifecycle Methods ------------------------------
|
|
|
|
public void init()
|
|
{
|
|
PropertyCheck.mandatory(this, "imapConfigMountPoints", imapConfigMountPoints);
|
|
PropertyCheck.mandatory(this, "ignoreExtractionFoldersBeans", ignoreExtractionFoldersBeans);
|
|
PropertyCheck.mandatory(this, "imapHome", imapHomeConfigBean);
|
|
|
|
PropertyCheck.mandatory(this, "fileFolderService", fileFolderService);
|
|
PropertyCheck.mandatory(this, "nodeService", nodeService);
|
|
PropertyCheck.mandatory(this, "permissionService", permissionService);
|
|
PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry);
|
|
PropertyCheck.mandatory(this, "defaultFromAddress", defaultFromAddress);
|
|
PropertyCheck.mandatory(this, "defaultToAddress", defaultToAddress);
|
|
PropertyCheck.mandatory(this, "repositoryTemplatePath", repositoryTemplatePath);
|
|
PropertyCheck.mandatory(this, "policyBehaviourFilter", policyBehaviourFilter);
|
|
PropertyCheck.mandatory(this, "mimetypeService", mimetypeService);
|
|
PropertyCheck.mandatory(this, "namespaceService", namespaceService);
|
|
PropertyCheck.mandatory(this, "searchService", getSearchService());
|
|
this.folderCache = new MaxSizeMap<Pair<String,String>, FolderStatus>(folderCacheSize, false);
|
|
|
|
// be sure that a default e-mail is correct
|
|
try
|
|
{
|
|
InternetAddress.parse(defaultFromAddress);
|
|
}
|
|
catch (AddressException ex)
|
|
{
|
|
throw new AlfrescoRuntimeException(
|
|
ERROR_CANNOT_PARSE_DEFAULT_EMAIL,
|
|
new Object[] {defaultFromAddress});
|
|
}
|
|
|
|
try
|
|
{
|
|
InternetAddress.parse(defaultToAddress);
|
|
}
|
|
catch (AddressException ex)
|
|
{
|
|
throw new AlfrescoRuntimeException(
|
|
ERROR_CANNOT_PARSE_DEFAULT_EMAIL,
|
|
new Object[] {defaultToAddress});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is run as System within a single transaction on startup.
|
|
*/
|
|
public void startup()
|
|
{
|
|
bindBehaviour();
|
|
|
|
// Get NodeRefs for folders to ignore
|
|
this.ignoreExtractionFolders = new HashSet<NodeRef>(ignoreExtractionFoldersBeans.length * 2);
|
|
|
|
for (RepositoryFolderConfigBean ignoreExtractionFoldersBean : ignoreExtractionFoldersBeans)
|
|
{
|
|
NodeRef nodeRef = ignoreExtractionFoldersBean.getFolderPath(namespaceService, nodeService, searchService,
|
|
fileFolderService);
|
|
|
|
if (!ignoreExtractionFolders.add(nodeRef))
|
|
{
|
|
// It was already in the set
|
|
throw new AlfrescoRuntimeException("The folder extraction path has been referenced already: \n"
|
|
+ " Folder: " + ignoreExtractionFoldersBean);
|
|
}
|
|
}
|
|
|
|
// Locate or create IMAP home
|
|
imapHomeNodeRef = imapHomeConfigBean.getOrCreateFolderPath(namespaceService, nodeService, searchService, fileFolderService);
|
|
}
|
|
|
|
public void shutdown()
|
|
{
|
|
}
|
|
|
|
protected void startupInTxn(boolean force)
|
|
{
|
|
if (force || getImapServerEnabled())
|
|
{
|
|
AuthenticationUtil.runAs(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
List<AlfrescoImapFolder> mailboxes = serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(
|
|
new RetryingTransactionCallback<List<AlfrescoImapFolder>>()
|
|
{
|
|
@Override
|
|
public List<AlfrescoImapFolder> execute() throws Throwable
|
|
{
|
|
startup();
|
|
|
|
List<AlfrescoImapFolder> result = new LinkedList<AlfrescoImapFolder>();
|
|
|
|
// Hit the mount points and warm the caches for early failure
|
|
for (String mountPointName : imapConfigMountPoints.keySet())
|
|
{
|
|
result.addAll(listMailboxes(new AlfrescoImapUser(null, AuthenticationUtil
|
|
.getSystemUserName(), null), mountPointName + "*", false));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Let each mailbox search trigger its own distinct transaction
|
|
for (AlfrescoImapFolder mailbox : mailboxes)
|
|
{
|
|
mailbox.getUidNext();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}, AuthenticationUtil.getSystemUserName());
|
|
}
|
|
}
|
|
|
|
protected void bindBehaviour()
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[bindBeahaviour] Binding behaviours");
|
|
}
|
|
PolicyComponent policyComponent = (PolicyComponent) serviceRegistry.getService(QName.createQName(NamespaceService.ALFRESCO_URI, "policyComponent"));
|
|
|
|
// Only listen to folders we've tagged with imap properties - not all folders or we'll really slow down the repository!
|
|
policyComponent.bindAssociationBehaviour(
|
|
OnCreateChildAssociationPolicy.QNAME,
|
|
ImapModel.ASPECT_IMAP_FOLDER,
|
|
ContentModel.ASSOC_CONTAINS,
|
|
new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.EVERY_EVENT));
|
|
policyComponent.bindAssociationBehaviour(
|
|
OnDeleteChildAssociationPolicy.QNAME,
|
|
ImapModel.ASPECT_IMAP_FOLDER,
|
|
ContentModel.ASSOC_CONTAINS,
|
|
new JavaBehaviour(this, "onDeleteChildAssociation", NotificationFrequency.EVERY_EVENT));
|
|
policyComponent.bindClassBehaviour(
|
|
OnUpdatePropertiesPolicy.QNAME,
|
|
ContentModel.TYPE_CONTENT,
|
|
new JavaBehaviour(this, "onUpdateProperties", NotificationFrequency.EVERY_EVENT));
|
|
policyComponent.bindClassBehaviour(
|
|
BeforeDeleteNodePolicy.QNAME,
|
|
ContentModel.TYPE_CONTENT,
|
|
new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT));
|
|
}
|
|
|
|
// ---------------------- Service Methods --------------------------------
|
|
|
|
public SimpleStoredMessage getMessage(FileInfo mesInfo) throws MessagingException
|
|
{
|
|
NodeRef nodeRef = mesInfo.getNodeRef();
|
|
Date modified = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
|
|
if(modified != null)
|
|
{
|
|
CacheItem cached = messageCache.get(nodeRef);
|
|
if (cached != null)
|
|
{
|
|
if (cached.getModified().equals(modified))
|
|
{
|
|
return cached.getMessage();
|
|
}
|
|
}
|
|
SimpleStoredMessage message = createImapMessage(mesInfo, true);
|
|
messageCache.put(nodeRef, new CacheItem(modified, message));
|
|
return message;
|
|
}
|
|
else
|
|
{
|
|
SimpleStoredMessage message = createImapMessage(mesInfo, true);
|
|
return message;
|
|
}
|
|
}
|
|
|
|
public SimpleStoredMessage createImapMessage(FileInfo fileInfo, boolean generateBody) throws MessagingException
|
|
{
|
|
// TODO MER 26/11/2010- this test should really be that the content of the node is of type message/RFC822
|
|
Long key = (Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID);
|
|
if (nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT))
|
|
{
|
|
return new SimpleStoredMessage(new ImapModelMessage(fileInfo, serviceRegistry, generateBody), new Date(), key);
|
|
}
|
|
else
|
|
{
|
|
return new SimpleStoredMessage(new ContentModelMessage(fileInfo, serviceRegistry, generateBody), new Date(), key);
|
|
}
|
|
}
|
|
|
|
public void expungeMessage(FileInfo fileInfo)
|
|
{
|
|
Flags flags = getFlags(fileInfo);
|
|
if (flags.contains(Flags.Flag.DELETED))
|
|
{
|
|
fileFolderService.delete(fileInfo.getNodeRef());
|
|
messageCache.remove(fileInfo.getNodeRef());
|
|
}
|
|
}
|
|
|
|
public AlfrescoImapFolder getOrCreateMailbox(AlfrescoImapUser user, String mailboxName, boolean mayExist, boolean mayCreate)
|
|
{
|
|
if (mailboxName == null)
|
|
{
|
|
throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY));
|
|
}
|
|
// A request for the hierarchy delimiter
|
|
if (mailboxName.length() == 0)
|
|
{
|
|
return new AlfrescoImapFolder(user.getLogin(), this, serviceRegistry);
|
|
}
|
|
final NodeRef root;
|
|
final List<String> pathElements;
|
|
ImapViewMode viewMode = ImapViewMode.ARCHIVE;
|
|
int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
|
|
int mountPointId = 0;
|
|
if (index < 0)
|
|
{
|
|
root = getUserImapHomeRef(user.getLogin());
|
|
pathElements = Collections.singletonList(mailboxName);
|
|
}
|
|
else
|
|
{
|
|
String rootPath = mailboxName.substring(0, index);
|
|
ImapConfigMountPointsBean imapConfigMountPoint = this.imapConfigMountPoints.get(rootPath);
|
|
if (imapConfigMountPoint != null)
|
|
{
|
|
mountPointId = this.mountPointIds.get(rootPath);
|
|
root = imapConfigMountPoint.getFolderPath(serviceRegistry.getNamespaceService(), nodeService, searchService, fileFolderService);
|
|
pathElements = Arrays.asList(mailboxName.substring(index + 1).split(
|
|
String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)));
|
|
viewMode = imapConfigMountPoint.getMode();
|
|
}
|
|
else
|
|
{
|
|
root = getUserImapHomeRef(user.getLogin());
|
|
pathElements = Arrays.asList(mailboxName.split(String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)));
|
|
}
|
|
}
|
|
FileInfo mailFolder;
|
|
try
|
|
{
|
|
mailFolder = fileFolderService.resolveNamePath(root, pathElements, !mayCreate);
|
|
}
|
|
catch (FileNotFoundException e)
|
|
{
|
|
throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[]
|
|
{
|
|
mailboxName
|
|
});
|
|
}
|
|
if (mailFolder == null)
|
|
{
|
|
if (!mayCreate)
|
|
{
|
|
throw new AlfrescoRuntimeException(ERROR_CANNOT_GET_A_FOLDER, new String[]
|
|
{
|
|
mailboxName
|
|
});
|
|
}
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Creating mailbox: " + mailboxName);
|
|
}
|
|
mailFolder = FileFolderUtil.makeFolders(fileFolderService, root, pathElements, ContentModel.TYPE_FOLDER);
|
|
}
|
|
else
|
|
{
|
|
if (!mayExist)
|
|
{
|
|
throw new AlfrescoRuntimeException(ERROR_FOLDER_ALREADY_EXISTS);
|
|
}
|
|
}
|
|
return new AlfrescoImapFolder(mailFolder, user.getLogin(), pathElements.get(pathElements.size() - 1), mailboxName, viewMode,
|
|
this, serviceRegistry, true, isExtractionEnabled(mailFolder.getNodeRef()), mountPointId);
|
|
}
|
|
|
|
public void deleteMailbox(AlfrescoImapUser user, String mailboxName)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Deleting mailbox: mailboxName=" + mailboxName);
|
|
}
|
|
if (mailboxName == null)
|
|
{
|
|
throw new IllegalArgumentException(I18NUtil.getMessage(ERROR_MAILBOX_NAME_IS_MANDATORY));
|
|
}
|
|
|
|
AlfrescoImapFolder folder = getOrCreateMailbox(user, mailboxName, true, false);
|
|
NodeRef nodeRef = folder.getFolderInfo().getNodeRef();
|
|
|
|
List<FileInfo> childFolders = fileFolderService.listFolders(nodeRef);
|
|
|
|
if (childFolders.isEmpty())
|
|
{
|
|
folder.signalDeletion();
|
|
// Delete child folders and messages
|
|
fileFolderService.delete(nodeRef);
|
|
}
|
|
else
|
|
{
|
|
if (folder.isSelectable())
|
|
{
|
|
// Delete all messages for this folder
|
|
// Don't delete subfolders and their messages
|
|
List<FileInfo> messages = fileFolderService.listFiles(nodeRef);
|
|
for (FileInfo message : messages)
|
|
{
|
|
fileFolderService.delete(message.getNodeRef());
|
|
}
|
|
nodeService.addAspect(nodeRef, ImapModel.ASPECT_IMAP_FOLDER_NONSELECTABLE, null);
|
|
}
|
|
else
|
|
{
|
|
throw new AlfrescoRuntimeException(mailboxName + " - Can't delete a non-selectable store with children.");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void renameMailbox(AlfrescoImapUser user, String oldMailboxName, String newMailboxName)
|
|
{
|
|
if (oldMailboxName == null || newMailboxName == null)
|
|
{
|
|
throw new IllegalArgumentException(ERROR_MAILBOX_NAME_IS_MANDATORY);
|
|
}
|
|
|
|
AlfrescoImapFolder sourceNode = getOrCreateMailbox(user, oldMailboxName, true, false);
|
|
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Renaming folder oldMailboxName=" + oldMailboxName + " newMailboxName=" + newMailboxName);
|
|
}
|
|
|
|
NodeRef newMailParent;
|
|
String newMailName;
|
|
int index = newMailboxName.lastIndexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
|
|
if (index < 0)
|
|
{
|
|
newMailParent = getUserImapHomeRef(user.getLogin());
|
|
newMailName = newMailboxName;
|
|
}
|
|
else
|
|
{
|
|
newMailParent = getOrCreateMailbox(user, newMailboxName.substring(0, index), true, true).getFolderInfo().getNodeRef();
|
|
newMailName = newMailboxName.substring(index + 1);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (oldMailboxName.equalsIgnoreCase(AlfrescoImapConst.INBOX_NAME))
|
|
{
|
|
// If you trying to rename INBOX
|
|
// - just copy it to another folder with new name
|
|
// and leave INBOX (with children) intact.
|
|
fileFolderService.copy(sourceNode.getFolderInfo().getNodeRef(), newMailParent,
|
|
AlfrescoImapConst.INBOX_NAME);
|
|
}
|
|
else
|
|
{
|
|
fileFolderService.move(sourceNode.getFolderInfo().getNodeRef(), newMailParent, newMailName);
|
|
}
|
|
}
|
|
catch (FileNotFoundException e)
|
|
{
|
|
throw new AlfrescoRuntimeException(e.getMessage(), e);
|
|
}
|
|
catch (FileExistsException e)
|
|
{
|
|
throw new AlfrescoRuntimeException(e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search for emails in specified folder depending on view mode.
|
|
*
|
|
* Shallow list of files
|
|
*
|
|
* @param contextNodeRef context folder for search
|
|
* @param viewMode context folder view mode
|
|
* @return list of emails that context folder contains.
|
|
*/
|
|
public FolderStatus getFolderStatus(final String userName, final NodeRef contextNodeRef, ImapViewMode viewMode)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("getFolderStatus contextNodeRef=" + contextNodeRef + ", viewMode=" + viewMode);
|
|
}
|
|
|
|
// No need to ACL check the change token read
|
|
String changeToken = AuthenticationUtil.runAs(new RunAsWork<String>()
|
|
{
|
|
@Override
|
|
public String doWork() throws Exception
|
|
{
|
|
return (String) nodeService.getProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN);
|
|
}
|
|
}, AuthenticationUtil.getSystemUserName());
|
|
|
|
Pair<String, String> cacheKey = null;
|
|
if (changeToken != null)
|
|
{
|
|
cacheKey = new Pair<String, String>(userName, changeToken);
|
|
this.folderCacheLock.readLock().lock();
|
|
try
|
|
{
|
|
FolderStatus result = this.folderCache.get(cacheKey);
|
|
if (result != null)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
this.folderCacheLock.readLock().unlock();
|
|
}
|
|
}
|
|
|
|
List<FileInfo> fileInfos = null;
|
|
FileFilterMode.setClient(Client.imap);
|
|
try
|
|
{
|
|
fileInfos = fileFolderService.listFiles(contextNodeRef);
|
|
}
|
|
finally
|
|
{
|
|
FileFilterMode.clearClient();
|
|
}
|
|
|
|
final NavigableMap<Long, FileInfo> currentSearch = new TreeMap<Long, FileInfo>();
|
|
|
|
switch (viewMode)
|
|
{
|
|
case MIXED:
|
|
for (FileInfo fileInfo : fileInfos)
|
|
{
|
|
currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo);
|
|
}
|
|
break;
|
|
case ARCHIVE:
|
|
for (FileInfo fileInfo : fileInfos)
|
|
{
|
|
if (nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT))
|
|
{
|
|
currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo);
|
|
}
|
|
}
|
|
break;
|
|
case VIRTUAL:
|
|
for (FileInfo fileInfo : fileInfos)
|
|
{
|
|
if (!nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT))
|
|
{
|
|
currentSearch.put((Long) fileInfo.getProperties().get(ContentModel.PROP_NODE_DBID), fileInfo);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
int messageCount = currentSearch.size(), recentCount = 0, unseenCount = 0, firstUnseen = 0;
|
|
int i = 1;
|
|
for (FileInfo fileInfo : currentSearch.values())
|
|
{
|
|
Flags flags = getFlags(fileInfo);
|
|
if (flags.contains(Flags.Flag.RECENT))
|
|
{
|
|
recentCount++;
|
|
}
|
|
if (!flags.contains(Flags.Flag.SEEN))
|
|
{
|
|
if (firstUnseen == 0)
|
|
{
|
|
firstUnseen = i;
|
|
}
|
|
unseenCount++;
|
|
}
|
|
i++;
|
|
}
|
|
// Add the IMAP folder aspect with appropriate initial values if it is not already there
|
|
if (changeToken == null)
|
|
{
|
|
changeToken = GUID.generate();
|
|
cacheKey = new Pair<String, String>(userName, changeToken);
|
|
final String finalToken = changeToken;
|
|
doAsSystem(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
nodeService.setProperty(contextNodeRef, ImapModel.PROP_CHANGE_TOKEN, finalToken);
|
|
nodeService.setProperty(contextNodeRef, ImapModel.PROP_MAXUID, currentSearch.isEmpty() ? 0
|
|
: currentSearch.lastKey());
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
Long uidValidity = (Long) nodeService.getProperty(contextNodeRef, ImapModel.PROP_UIDVALIDITY);
|
|
FolderStatus result = new FolderStatus(messageCount, recentCount, firstUnseen, unseenCount,
|
|
uidValidity == null ? 0 : uidValidity, changeToken, currentSearch);
|
|
this.folderCacheLock.writeLock().lock();
|
|
try
|
|
{
|
|
FolderStatus oldResult = this.folderCache.get(cacheKey);
|
|
if (oldResult != null)
|
|
{
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("At end of getFolderStatus. Found info in cache, changeToken:" + changeToken);
|
|
}
|
|
|
|
return oldResult;
|
|
}
|
|
this.folderCache.put(cacheKey, result);
|
|
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("At end of getFolderStatus. Found files:" + currentSearch.size() + ", changeToken:" + changeToken);
|
|
}
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
this.folderCacheLock.writeLock().unlock();
|
|
}
|
|
}
|
|
|
|
public void subscribe(AlfrescoImapUser user, String mailbox)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Subscribing: " + user + ", " + mailbox);
|
|
}
|
|
AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false);
|
|
PersonService personService = serviceRegistry.getPersonService();
|
|
NodeRef userRef = personService.getPerson(user.getLogin());
|
|
|
|
nodeService.removeAssociation(userRef, mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASSOC_IMAP_UNSUBSCRIBED);
|
|
}
|
|
|
|
public void unsubscribe(AlfrescoImapUser user, String mailbox)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Unsubscribing: " + user + ", " + mailbox);
|
|
}
|
|
AlfrescoImapFolder mailFolder = getOrCreateMailbox(user, mailbox, true, false);
|
|
if(mailFolder.getFolderInfo() != null)
|
|
{
|
|
PersonService personService = serviceRegistry.getPersonService();
|
|
NodeRef userRef = personService.getPerson(user.getLogin());
|
|
nodeService.createAssociation(userRef, mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASSOC_IMAP_UNSUBSCRIBED);
|
|
}
|
|
else
|
|
{
|
|
// perhaps the folder has been deleted by another async process?
|
|
logger.debug("Unable to find folder to unsubscribe");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return flags that belong to the specified imap folder.
|
|
*
|
|
* @param messageInfo imap folder info.
|
|
* @return flags.
|
|
*/
|
|
public Flags getFlags(FileInfo messageInfo)
|
|
{
|
|
Flags flags = new Flags();
|
|
Map<QName, Serializable> props = nodeService.getProperties(messageInfo.getNodeRef());
|
|
|
|
for (QName key : qNameToFlag.keySet())
|
|
{
|
|
Boolean value = (Boolean) props.get(key);
|
|
if (value != null && value)
|
|
{
|
|
flags.add(qNameToFlag.get(key));
|
|
}
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
/**
|
|
* Set flags to the specified imapFolder.
|
|
*
|
|
* @param messageInfo FileInfo of imap Folder.
|
|
* @param flags flags to set.
|
|
* @param value value to set.
|
|
*/
|
|
public void setFlags(FileInfo messageInfo, Flags flags, boolean value)
|
|
{
|
|
checkForFlaggableAspect(messageInfo.getNodeRef());
|
|
|
|
|
|
for (Flags.Flag flag : flags.getSystemFlags())
|
|
{
|
|
setFlag(messageInfo, flag, value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set flags to the specified message.
|
|
*
|
|
* @param messageInfo FileInfo of imap Folder
|
|
* @param flag flag to set.
|
|
* @param value value value to set.
|
|
*/
|
|
public void setFlag(FileInfo messageInfo, Flag flag, boolean value)
|
|
{
|
|
setFlag(messageInfo.getNodeRef(), flag, value);
|
|
}
|
|
|
|
private void setFlag(NodeRef nodeRef, Flag flag, boolean value)
|
|
{
|
|
String permission = (flag == Flag.DELETED ? PermissionService.DELETE_NODE : PermissionService.WRITE_PROPERTIES);
|
|
|
|
|
|
AccessStatus status = permissionService.hasPermission(nodeRef, permission);
|
|
if (status == AccessStatus.DENIED)
|
|
{
|
|
if(flag == Flag.DELETED)
|
|
{
|
|
logger.debug("[setFlag] Access denied to set DELETED FLAG:" + nodeRef);
|
|
throw new AccessDeniedException("No permission to set DELETED flag");
|
|
}
|
|
if(flag == Flag.SEEN)
|
|
{
|
|
logger.debug("[setFlag] Access denied to set SEEN FLAG:" + nodeRef);
|
|
//TODO - should we throw an exception here?
|
|
//throw new AccessDeniedException("No permission to set DELETED flag");
|
|
}
|
|
else
|
|
{
|
|
|
|
logger.debug("[setFlag] Access denied to set flag:" + nodeRef);
|
|
throw new AccessDeniedException("No permission to set flag:" + flag.toString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
checkForFlaggableAspect(nodeRef);
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
|
|
try
|
|
{
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("set flag nodeRef:" + nodeRef + ",flag:" + flagToQname.get(flag) + ", value:" + value);
|
|
}
|
|
nodeService.setProperty(nodeRef, flagToQname.get(flag), value);
|
|
messageCache.remove(nodeRef);
|
|
}
|
|
finally
|
|
{
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Depend on listSubscribed param, list Mailboxes or list subscribed Mailboxes
|
|
*/
|
|
public List<AlfrescoImapFolder> listMailboxes(AlfrescoImapUser user, String mailboxPattern, boolean listSubscribed)
|
|
{
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[listMailboxes] user:" + user.getLogin() + ", mailboxPattern:" + mailboxPattern + ", listSubscribed:" + listSubscribed);
|
|
}
|
|
List<AlfrescoImapFolder> result = new LinkedList<AlfrescoImapFolder>();
|
|
|
|
// List mailboxes that are in mount points
|
|
int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
|
|
String rootPath = index == -1 ? mailboxPattern : mailboxPattern.substring(0, index);
|
|
boolean found = false;
|
|
|
|
String userName = user.getLogin();
|
|
Set<NodeRef> unsubscribedFodlers = getUnsubscribedFolders(userName);
|
|
|
|
for (String mountPointName : imapConfigMountPoints.keySet())
|
|
{
|
|
if (mountPointName.matches(rootPath.replaceAll("[%\\*]", ".*")))
|
|
{
|
|
NodeRef mountPoint = getMountPoint(mountPointName);
|
|
if (mountPoint != null)
|
|
{
|
|
int mountPointId = mountPointIds.get(mountPointName);
|
|
FileInfo mountPointFileInfo = fileFolderService.getFileInfo(mountPoint);
|
|
ImapViewMode viewMode = imapConfigMountPoints.get(mountPointName).getMode();
|
|
if (index < 0)
|
|
{
|
|
|
|
if (!listSubscribed || !unsubscribedFodlers.contains(mountPointFileInfo.getNodeRef()))
|
|
{
|
|
result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode,
|
|
isExtractionEnabled(mountPointFileInfo.getNodeRef()), this, serviceRegistry, mountPointId));
|
|
}
|
|
else if (rootPath.endsWith("%") && !expandFolder(mountPoint, user, mountPointName, "%", true, viewMode, mountPointId).isEmpty()) // \NoSelect
|
|
{
|
|
result.add(new AlfrescoImapFolder(mountPointFileInfo, userName, mountPointName, mountPointName, viewMode,
|
|
this, serviceRegistry, false, isExtractionEnabled(mountPointFileInfo.getNodeRef()), mountPointId));
|
|
}
|
|
if (rootPath.endsWith("*"))
|
|
{
|
|
result.addAll(expandFolder(mountPoint, user, mountPointName, "*", listSubscribed, viewMode, mountPointId));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.addAll(expandFolder(mountPoint, user, mountPointName,
|
|
mailboxPattern.substring(index + 1), listSubscribed, viewMode, mountPointId));
|
|
}
|
|
}
|
|
// If we had an exact match, there is no point continuing to search
|
|
if (mountPointName.equals(rootPath))
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// List mailboxes that are in user IMAP Home
|
|
if (!found)
|
|
{
|
|
NodeRef root = getUserImapHomeRef(user.getLogin());
|
|
result.addAll(expandFolder(root, user, "", mailboxPattern, listSubscribed, ImapViewMode.ARCHIVE, 0));
|
|
}
|
|
|
|
logger.debug("listMailboxes returning size:" + result.size());
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Recursively search the given root to get a list of folders
|
|
*
|
|
* @return
|
|
*/
|
|
private List<AlfrescoImapFolder> expandFolder(
|
|
NodeRef root,
|
|
AlfrescoImapUser user,
|
|
String rootPath,
|
|
String mailboxPattern,
|
|
boolean listSubscribed,
|
|
ImapViewMode viewMode,
|
|
int mountPointId)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("expand folder: root:" + root + " user: " + user + " :mailboxPattern=" + mailboxPattern);
|
|
}
|
|
if (mailboxPattern == null)
|
|
return null;
|
|
int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER);
|
|
|
|
String name = null;
|
|
if (index < 0)
|
|
{
|
|
name = mailboxPattern;
|
|
}
|
|
else
|
|
{
|
|
name = mailboxPattern.substring(0, index);
|
|
}
|
|
String rootPathPrefix = rootPath.length() == 0 ? "" : rootPath + AlfrescoImapConst.HIERARCHY_DELIMITER;
|
|
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Listing mailboxes: name=" + name);
|
|
}
|
|
|
|
List<AlfrescoImapFolder> fullList = new LinkedList<AlfrescoImapFolder>();
|
|
ImapSubFolderFilter filter = new ImapSubFolderFilter(viewMode, name.replace('%', '*'));
|
|
List<FileInfo> list;
|
|
// Only list this folder if we have a wildcard name. Otherwise do a direct lookup by name.
|
|
if (name.contains("*") || name.contains("%"))
|
|
{
|
|
FileFilterMode.setClient(Client.imap);
|
|
try
|
|
{
|
|
list = fileFolderService.listFolders(root);
|
|
}
|
|
finally
|
|
{
|
|
FileFilterMode.clearClient();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NodeRef nodeRef = fileFolderService.searchSimple(root, name);
|
|
FileInfo fileInfo;
|
|
list = nodeRef == null || !(fileInfo = fileFolderService.getFileInfo(nodeRef)).isFolder() ? Collections.<FileInfo>emptyList() : Collections.singletonList(fileInfo);
|
|
}
|
|
|
|
if (index < 0)
|
|
{
|
|
String userName = user.getLogin();
|
|
Set<NodeRef> unsubscribedFodlers = getUnsubscribedFolders(userName);
|
|
|
|
// This is the last level
|
|
for (FileInfo fileInfo : list)
|
|
{
|
|
if (!filter.isEnterSubfolder(fileInfo.getNodeRef()))
|
|
{
|
|
continue;
|
|
}
|
|
String folderPath = rootPathPrefix + fileInfo.getName();
|
|
if (!listSubscribed || !unsubscribedFodlers.contains(fileInfo.getNodeRef()))
|
|
{
|
|
fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode,
|
|
isExtractionEnabled(fileInfo.getNodeRef()), this, serviceRegistry, mountPointId));
|
|
}
|
|
else if (name.endsWith("%") && !expandFolder(fileInfo.getNodeRef(), user, folderPath, "%", true, viewMode, mountPointId).isEmpty()) // \NoSelect
|
|
{
|
|
fullList.add(new AlfrescoImapFolder(fileInfo, userName, fileInfo.getName(), folderPath, viewMode,
|
|
this, serviceRegistry, false, isExtractionEnabled(fileInfo.getNodeRef()), mountPointId));
|
|
}
|
|
if (name.endsWith("*"))
|
|
{
|
|
fullList.addAll(expandFolder(fileInfo.getNodeRef(), user, folderPath, "*", listSubscribed, viewMode, mountPointId));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If (index != -1) this is not the last level
|
|
for (FileInfo folder : list)
|
|
{
|
|
if (!filter.isEnterSubfolder(folder.getNodeRef()))
|
|
{
|
|
continue;
|
|
}
|
|
fullList.addAll(expandFolder(folder.getNodeRef(), user, rootPathPrefix + folder.getName(),
|
|
mailboxPattern.substring(index + 1), listSubscribed, viewMode, mountPointId));
|
|
}
|
|
}
|
|
return fullList;
|
|
}
|
|
|
|
/**
|
|
* Map of mount points. Name of mount point == key in the map.
|
|
*
|
|
* @return Map of mount points.
|
|
*/
|
|
private NodeRef getMountPoint(String rootFolder)
|
|
{
|
|
final ImapConfigMountPointsBean config = imapConfigMountPoints.get(rootFolder);
|
|
try
|
|
{
|
|
// Get node reference. Do it in new transaction to avoid RollBack in case when AccessDeniedException is thrown.
|
|
return serviceRegistry.getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<NodeRef>()
|
|
{
|
|
public NodeRef execute() throws Exception
|
|
{
|
|
try
|
|
{
|
|
return config.getFolderPath(namespaceService, nodeService, searchService, fileFolderService);
|
|
}
|
|
catch (AccessDeniedException e)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("A mount point is skipped due to Access Dennied. \n" + " Mount point: " + config + "\n" + " User: "
|
|
+ AuthenticationUtil.getFullyAuthenticatedUser());
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}, true, true);
|
|
}
|
|
catch (AccessDeniedException e)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("A mount point is skipped due to Access Dennied. \n" + " Mount point: " + config + "\n" + " User: "
|
|
+ AuthenticationUtil.getFullyAuthenticatedUser());
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the node ref of the user's imap home. Will create it on demand if it
|
|
* does not already exist.
|
|
*
|
|
* @param userName user name
|
|
* @return user IMAP home reference and create it if it doesn't exist.
|
|
*/
|
|
public NodeRef getUserImapHomeRef(final String userName)
|
|
{
|
|
NodeRef userHome = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<NodeRef>()
|
|
{
|
|
public NodeRef doWork() throws Exception
|
|
{
|
|
// Look for user imap home
|
|
NodeRef userHome = fileFolderService.searchSimple(imapHomeNodeRef, userName);
|
|
if (userHome == null)
|
|
{
|
|
// user imap home does not exist
|
|
NodeRef result = fileFolderService.create(imapHomeNodeRef, userName, ContentModel.TYPE_FOLDER).getNodeRef();
|
|
nodeService.setProperty(result, ContentModel.PROP_DESCRIPTION, userName);
|
|
|
|
// create user inbox
|
|
fileFolderService.create(result, AlfrescoImapConst.INBOX_NAME, ContentModel.TYPE_FOLDER);
|
|
|
|
// Set permissions on user's imap home
|
|
permissionService.setInheritParentPermissions(result, false);
|
|
permissionService.setPermission(result, PermissionService.OWNER_AUTHORITY, PermissionService.ALL_PERMISSIONS, true);
|
|
|
|
return result;
|
|
}
|
|
|
|
return userHome;
|
|
}
|
|
}, AuthenticationUtil.getSystemUserName());
|
|
|
|
return userHome;
|
|
}
|
|
|
|
private Set<NodeRef> getUnsubscribedFolders(String userName)
|
|
{
|
|
Set<NodeRef> result = new HashSet<NodeRef>();
|
|
PersonService personService = serviceRegistry.getPersonService();
|
|
NodeRef userRef = personService.getPerson(userName);
|
|
List<AssociationRef> unsubscribedFodlers = nodeService.getTargetAssocs(userRef, ImapModel.ASSOC_IMAP_UNSUBSCRIBED);
|
|
for (AssociationRef asocRef : unsubscribedFodlers)
|
|
{
|
|
result.add(asocRef.getTargetRef());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
private String getCurrentUser()
|
|
{
|
|
return AuthenticationUtil.getFullyAuthenticatedUser();
|
|
}
|
|
|
|
/**
|
|
* Return list of "favourite" sites, that belong to the specified user and are marked as "Imap favourite"
|
|
*
|
|
* @param userName name of user
|
|
* @return List of favourite sites.
|
|
*/
|
|
private List<NodeRef> getFavouriteSites(final String userName)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[getFavouriteSites] entry for user: " + userName);
|
|
}
|
|
List<NodeRef> favSites = AlfrescoTransactionSupport.getResource(FAVORITE_SITES);
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
if (favSites == null)
|
|
{
|
|
logger.debug("[getFavouriteSites] There is no Favorite sites' list bound to transaction " + AlfrescoTransactionSupport.getTransactionId());
|
|
}
|
|
else
|
|
{
|
|
logger.debug("[getFavouriteSites] Found Favorite sites' list bound to transaction " + AlfrescoTransactionSupport.getTransactionId());
|
|
}
|
|
}
|
|
if (favSites == null)
|
|
{
|
|
favSites = new LinkedList<NodeRef>();
|
|
|
|
PreferenceService preferenceService = (PreferenceService) serviceRegistry
|
|
.getService(ServiceRegistry.PREFERENCE_SERVICE);
|
|
Map<String, Serializable> prefs = preferenceService.getPreferences(
|
|
userName, AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES);
|
|
|
|
/**
|
|
* List the user's sites
|
|
*/
|
|
List<SiteInfo> sites = serviceRegistry.getTransactionService()
|
|
.getRetryingTransactionHelper().doInTransaction(
|
|
new RetryingTransactionCallback<List<SiteInfo>>()
|
|
{
|
|
public List<SiteInfo> execute() throws Exception
|
|
{
|
|
List<SiteInfo> res = new ArrayList<SiteInfo>();
|
|
try
|
|
{
|
|
|
|
res = serviceRegistry.getSiteService()
|
|
.listSites(userName);
|
|
}
|
|
catch (SiteServiceException e)
|
|
{
|
|
// Do nothing. Root sites folder was not
|
|
// created.
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.warn("[getFavouriteSites] Root sites folder was not created.");
|
|
}
|
|
}
|
|
catch (InvalidNodeRefException e)
|
|
{
|
|
// Do nothing. Root sites folder was
|
|
// deleted.
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.warn("[getFavouriteSites] Root sites folder was deleted.");
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
}, false, true);
|
|
|
|
for (SiteInfo siteInfo : sites)
|
|
{
|
|
String key = AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES + "."
|
|
+ siteInfo.getShortName();
|
|
Boolean isImapFavourite = (Boolean) prefs.get(key);
|
|
if (isImapFavourite != null && isImapFavourite)
|
|
{
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[getFavouriteSites] User: " + userName + " Favourite site: " + siteInfo.getShortName());
|
|
}
|
|
favSites.add(siteInfo.getNodeRef());
|
|
}
|
|
}
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[getFavouriteSites] Bind new Favorite sites' list to transaction " + AlfrescoTransactionSupport.getTransactionId());
|
|
}
|
|
AlfrescoTransactionSupport.bindResource(FAVORITE_SITES, favSites);
|
|
}
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[getFavouriteSites] end for user: " + userName);
|
|
}
|
|
|
|
return favSites;
|
|
}
|
|
|
|
/**
|
|
* Checks for the existence of the flaggable aspect and adds it if it is not already present on the folder.
|
|
* @param nodeRef
|
|
*/
|
|
private void checkForFlaggableAspect(NodeRef nodeRef)
|
|
{
|
|
Set<NodeRef> alreadyChecked = AlfrescoTransactionSupport.getResource(CHECKED_NODES);
|
|
if (alreadyChecked == null)
|
|
{
|
|
alreadyChecked = new HashSet<NodeRef>();
|
|
}
|
|
if (alreadyChecked.contains(nodeRef))
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[checkForFlaggableAspect] Flaggable aspect has been already checked for {" + nodeRef + "}");
|
|
}
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
serviceRegistry.getLockService().checkForLock(nodeRef);
|
|
}
|
|
catch (NodeLockedException e)
|
|
{
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[checkForFlaggableAspect] Node {" + nodeRef + "} is locked");
|
|
}
|
|
alreadyChecked.add(nodeRef);
|
|
return;
|
|
}
|
|
if (!nodeService.hasAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE))
|
|
{
|
|
AccessStatus status = permissionService.hasPermission(nodeRef, PermissionService.WRITE_PROPERTIES);
|
|
if (status == AccessStatus.DENIED)
|
|
{
|
|
logger.debug("[checkForFlaggableAspect] No permissions to add FLAGGABLE aspect" + nodeRef);
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
|
|
logger.debug("[checkForFlaggableAspect] Adding flaggable aspect to nodeRef: " + nodeRef);
|
|
Map<QName, Serializable> aspectProperties = new HashMap<QName, Serializable>();
|
|
nodeService.addAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE, aspectProperties);
|
|
}
|
|
finally
|
|
{
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
|
|
}
|
|
}
|
|
}
|
|
alreadyChecked.add(nodeRef);
|
|
AlfrescoTransactionSupport.bindResource(CHECKED_NODES, alreadyChecked);
|
|
}
|
|
|
|
private boolean isExtractionEnabled(NodeRef nodeRef)
|
|
{
|
|
return extractAttachmentsEnabled && !ignoreExtractionFolders.contains(nodeRef);
|
|
}
|
|
|
|
public String getDefaultEmailBodyTemplate(EmailBodyFormat type)
|
|
{
|
|
if (defaultBodyTemplates == null)
|
|
{
|
|
defaultBodyTemplates = new HashMap<EmailBodyFormat, String>(4);
|
|
|
|
for (EmailBodyFormat onetype : EmailBodyFormat.values())
|
|
{
|
|
String result = onetype.getClasspathTemplatePath();
|
|
try
|
|
{
|
|
// This query uses cm:name to find the template node(s).
|
|
// For the case where the templates are renamed, it would be better to use a QName path-based query.
|
|
|
|
|
|
final StringBuilder templateName = new StringBuilder(DICTIONARY_TEMPLATE_PREFIX).append("_").append(onetype.getTypeSubtype()).append("_").append(onetype.getWebApp()).append(".ftl");
|
|
|
|
final String repositoryTemplatePath = getRepositoryTemplatePath();
|
|
int indexOfStoreDelim = repositoryTemplatePath.indexOf(StoreRef.URI_FILLER);
|
|
if (indexOfStoreDelim == -1)
|
|
{
|
|
throw new IllegalArgumentException("Bad path format, " + StoreRef.URI_FILLER + " not found");
|
|
}
|
|
indexOfStoreDelim += StoreRef.URI_FILLER.length();
|
|
int indexOfPathDelim = repositoryTemplatePath.indexOf("/", indexOfStoreDelim);
|
|
if (indexOfPathDelim == -1)
|
|
{
|
|
throw new IllegalArgumentException("Bad path format, '/' not found");
|
|
}
|
|
final String storePath = repositoryTemplatePath.substring(0, indexOfPathDelim);
|
|
final String rootPathInStore = repositoryTemplatePath.substring(indexOfPathDelim);
|
|
final String query = rootPathInStore + "/" + NamespaceService.CONTENT_MODEL_PREFIX + ":" + templateName;
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[getDefaultEmailBodyTemplate] Query: " + query);
|
|
}
|
|
StoreRef storeRef = new StoreRef(storePath);
|
|
|
|
NodeRef rootNode = nodeService.getRootNode(storeRef);
|
|
|
|
List<NodeRef> templates = searchService.selectNodes(rootNode, query, null, namespaceService, true);
|
|
if (templates == null || templates.size() == 0)
|
|
{
|
|
if(logger.isDebugEnabled())
|
|
{
|
|
logger.debug("template not found:" + templateName);
|
|
}
|
|
throw new AlfrescoRuntimeException(String.format("[getDefaultEmailBodyTemplate] IMAP message template '%1$s' does not exist in the path '%2$s'.", templateName, repositoryTemplatePath));
|
|
}
|
|
final NodeRef defaultLocaleTemplate = templates.get(0);
|
|
|
|
NodeRef localisedSibling = serviceRegistry.getFileFolderService().getLocalizedSibling(defaultLocaleTemplate);
|
|
result = localisedSibling.toString();
|
|
}
|
|
// We are catching all exceptions. E.g. search service can possibly throw an exceptions on malformed queries.
|
|
catch (Exception e)
|
|
{
|
|
logger.error("ImapServiceImpl [getDefaultEmailBodyTemplate]", e);
|
|
}
|
|
defaultBodyTemplates.put(onetype, result);
|
|
}
|
|
}
|
|
return defaultBodyTemplates.get(type);
|
|
}
|
|
|
|
/**
|
|
* This method should returns a unique identifier of Alfresco server. The possible UID may be calculated based on IP address, Server port, MAC address, Web Application context.
|
|
* This UID should be parseable into initial components. This necessary for the implementation of the following case: If the message being copied (e.g. drag-and-drop) between
|
|
* two different Alfresco accounts in the IMAP client, we must unambiguously identify from which Alfresco server this message being copied. The message itself does not contain
|
|
* content data, so we must download it from the initial server (e.g. using download content servlet) and save it into destination repository.
|
|
*
|
|
* @return String representation of unique identifier of Alfresco server
|
|
*/
|
|
public String getAlfrescoServerUID()
|
|
{
|
|
// TODO Implement as javadoc says.
|
|
return "Not-Implemented";
|
|
}
|
|
|
|
/**
|
|
* Share Site Exclusion Filter
|
|
*/
|
|
private class ImapSubFolderFilter implements SubFolderFilter
|
|
{
|
|
/**
|
|
* Exclude Share Sites of TYPE_SITE
|
|
*/
|
|
private Collection<QName> typesToExclude;
|
|
private List<NodeRef> favs;
|
|
private String mailboxPattern;
|
|
private ImapViewMode imapViewMode;
|
|
|
|
ImapSubFolderFilter(ImapViewMode imapViewMode)
|
|
{
|
|
this.imapViewMode = imapViewMode;
|
|
this.typesToExclude = ImapServiceImpl.this.serviceRegistry.getDictionaryService().getSubTypes(SiteModel.TYPE_SITE, true);
|
|
this.favs = getFavouriteSites(getCurrentUser());
|
|
}
|
|
|
|
ImapSubFolderFilter(ImapViewMode imapViewMode, String mailboxPattern)
|
|
{
|
|
this(imapViewMode);
|
|
this.mailboxPattern = mailboxPattern.replaceAll("\\*", "(.)*");;
|
|
}
|
|
|
|
@Override
|
|
public boolean isEnterSubfolder(ChildAssociationRef subfolderRef)
|
|
{
|
|
return isEnterSubfolder(subfolderRef.getChildRef());
|
|
}
|
|
|
|
public boolean isEnterSubfolder(NodeRef folder)
|
|
{
|
|
String name = (String) nodeService.getProperty(folder, ContentModel.PROP_NAME);
|
|
if (mailboxPattern != null)
|
|
{
|
|
logger.debug("Child name: " + name);
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("Folder name: " + name + ". Pattern: " + mailboxPattern + ". Matches: " + name.matches(mailboxPattern));
|
|
}
|
|
if (!name.matches(mailboxPattern))
|
|
return false;
|
|
}
|
|
QName typeOfFolder = nodeService.getType(folder);
|
|
if (typesToExclude.contains(typeOfFolder))
|
|
{
|
|
if (imapViewMode == ImapViewMode.VIRTUAL || imapViewMode == ImapViewMode.MIXED)
|
|
{
|
|
/**
|
|
* In VIRTUAL and MIXED MODE WE SHOULD ONLY DISPLAY FOLDERS FROM FAVOURITE SITES
|
|
*/
|
|
if (favs.contains(folder))
|
|
{
|
|
logger.debug("[ImapSubFolderFilter] (VIRTUAL) including fav site folder :" + name);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
logger.debug("[ImapSubFolderFilter] (VIRTUAL) excluding non fav site folder :" + name);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/**
|
|
* IN ARCHIVE MODE we don't display folders for any SITES, regardless of whether they are favourites.
|
|
*/
|
|
logger.debug("[ImapSubFolderFilter] (ARCHIVE) excluding site folder :" + name);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
private UidValidityTransactionListener getUidValidityTransactionListener(NodeRef folderRef)
|
|
{
|
|
String key = UIDVALIDITY_TRANSACTION_LISTENER + folderRef.toString();
|
|
UidValidityTransactionListener txnListener = AlfrescoTransactionSupport.getResource(key);
|
|
if (txnListener == null)
|
|
{
|
|
txnListener = new UidValidityTransactionListener(folderRef);
|
|
AlfrescoTransactionSupport.bindListener(txnListener);
|
|
AlfrescoTransactionSupport.bindResource(key, txnListener);
|
|
}
|
|
return txnListener;
|
|
}
|
|
|
|
@Override
|
|
public void onCreateChildAssociation(final ChildAssociationRef childAssocRef, boolean isNewNode)
|
|
{
|
|
doAsSystem(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
NodeRef childNodeRef = childAssocRef.getChildRef();
|
|
|
|
if (serviceRegistry.getDictionaryService().isSubClass(nodeService.getType(childNodeRef), ContentModel.TYPE_CONTENT))
|
|
{
|
|
long newId = (Long) nodeService.getProperty(childNodeRef, ContentModel.PROP_NODE_DBID);
|
|
// Keep a record of minimum and maximum node IDs in this folder in this transaction and add a listener that will
|
|
// update the UIDVALIDITY and MAXUID properties appropriately. Also force generation of a new change token
|
|
getUidValidityTransactionListener(childAssocRef.getParentRef()).recordNewUid(newId);
|
|
// Flag new content as recent
|
|
setFlag(childNodeRef, Flags.Flag.RECENT, true);
|
|
}
|
|
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[onCreateChildAssociation] Association " + childAssocRef + " created. CHANGETOKEN will be changed.");
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onDeleteChildAssociation(final ChildAssociationRef childAssocRef)
|
|
{
|
|
doAsSystem(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
NodeRef childNodeRef = childAssocRef.getChildRef();
|
|
if (serviceRegistry.getDictionaryService().isSubClass(nodeService.getType(childNodeRef),
|
|
ContentModel.TYPE_CONTENT))
|
|
{
|
|
// Force generation of a new change token
|
|
getUidValidityTransactionListener(childAssocRef.getParentRef());
|
|
|
|
// Remove the message from the cache
|
|
messageCache.remove(childNodeRef);
|
|
}
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("[onDeleteChildAssociation] Association " + childAssocRef
|
|
+ " created. CHANGETOKEN will be changed.");
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onUpdateProperties(final NodeRef nodeRef, final Map<QName, Serializable> before,
|
|
final Map<QName, Serializable> after)
|
|
{
|
|
doAsSystem(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
/**
|
|
* Imap only cares about a few properties however if those properties
|
|
* change then the uidvalidity needs to be reset otherwise the new content
|
|
* won't get re-loaded. This is nonsense for an email server, but needed for
|
|
* modifiable repository. Also we need to ignore certain properties.
|
|
*/
|
|
boolean hasChanged = false;
|
|
|
|
if(!hasChanged)
|
|
{
|
|
hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_NAME), after.get(ContentModel.PROP_NAME));
|
|
}
|
|
if(!hasChanged)
|
|
{
|
|
hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_AUTHOR), after.get(ContentModel.PROP_AUTHOR));
|
|
}
|
|
if(!hasChanged)
|
|
{
|
|
hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_TITLE), after.get(ContentModel.PROP_TITLE));
|
|
}
|
|
if(!hasChanged)
|
|
{
|
|
hasChanged = !EqualsHelper.nullSafeEquals(before.get(ContentModel.PROP_DESCRIPTION), after.get(ContentModel.PROP_DESCRIPTION));
|
|
}
|
|
|
|
if(!hasChanged)
|
|
{
|
|
Serializable s1 = before.get(ContentModel.PROP_CONTENT);
|
|
Serializable s2 = after.get(ContentModel.PROP_CONTENT);
|
|
|
|
if(s1 != null && s2 != null)
|
|
{
|
|
ContentData c1 = (ContentData)s1;
|
|
ContentData c2 = (ContentData)s2;
|
|
|
|
hasChanged = !EqualsHelper.nullSafeEquals(c1.getContentUrl(), c2.getContentUrl());
|
|
}
|
|
}
|
|
|
|
for (ChildAssociationRef parentAssoc : nodeService.getParentAssocs(nodeRef))
|
|
{
|
|
NodeRef folderRef = parentAssoc.getParentRef();
|
|
if (nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER))
|
|
{
|
|
messageCache.remove(nodeRef);
|
|
|
|
// Force generation of a new change token for the parent folders
|
|
UidValidityTransactionListener listener = getUidValidityTransactionListener(folderRef);
|
|
|
|
// if we have a significant change then we need to force a new uidvalidity.
|
|
if(hasChanged)
|
|
{
|
|
logger.debug("message has changed - force new uidvalidity for the parent folder");
|
|
listener.forceNewUidvalidity();
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void beforeDeleteNode(final NodeRef nodeRef)
|
|
{
|
|
doAsSystem(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
for (ChildAssociationRef parentAssoc : nodeService.getParentAssocs(nodeRef))
|
|
{
|
|
NodeRef folderRef = parentAssoc.getParentRef();
|
|
if (nodeService.hasAspect(folderRef, ImapModel.ASPECT_IMAP_FOLDER))
|
|
{
|
|
messageCache.remove(nodeRef);
|
|
|
|
// Force generation of a new change token
|
|
getUidValidityTransactionListener(folderRef);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
private <R> R doAsSystem(RunAsWork<R> work)
|
|
{
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
|
|
try
|
|
{
|
|
return AuthenticationUtil.runAs(work, AuthenticationUtil.getSystemUserName());
|
|
}
|
|
finally
|
|
{
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
|
|
policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
|
|
}
|
|
}
|
|
|
|
private class UidValidityTransactionListener extends TransactionListenerAdapter
|
|
{
|
|
// Generate a unique token for each folder change with which we can validate session caches
|
|
private String changeToken = GUID.generate();
|
|
private NodeRef folderNodeRef;
|
|
private Long minUid;
|
|
private Long maxUid;
|
|
private boolean forceNewUidValidity = false;
|
|
|
|
public UidValidityTransactionListener(NodeRef folderNodeRef)
|
|
{
|
|
this.folderNodeRef = folderNodeRef;
|
|
}
|
|
|
|
public void forceNewUidvalidity()
|
|
{
|
|
this.forceNewUidValidity = true;
|
|
}
|
|
|
|
public void recordNewUid(long newUid)
|
|
{
|
|
if (this.minUid == null)
|
|
{
|
|
this.minUid = this.maxUid = newUid;
|
|
}
|
|
else if (newUid < this.minUid)
|
|
{
|
|
this.minUid = newUid;
|
|
}
|
|
else if (newUid > this.maxUid)
|
|
{
|
|
this.maxUid = newUid;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void beforeCommit(boolean readOnly)
|
|
{
|
|
if (readOnly)
|
|
{
|
|
return;
|
|
}
|
|
|
|
doAsSystem(new RunAsWork<Void>()
|
|
{
|
|
@Override
|
|
public Void doWork() throws Exception
|
|
{
|
|
// This fires at the end of the transaction, so double-check that the node is still present
|
|
if (!nodeService.exists(folderNodeRef))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (UidValidityTransactionListener.this.forceNewUidValidity || UidValidityTransactionListener.this.minUid != null)
|
|
{
|
|
long modifDate = System.currentTimeMillis();
|
|
Long oldMax = (Long) nodeService.getProperty(folderNodeRef, ImapModel.PROP_MAXUID);
|
|
// Only update UIDVALIDITY if a new node has and ID that is smaller than the old maximum (as UIDs are always meant to increase)
|
|
if (UidValidityTransactionListener.this.forceNewUidValidity || oldMax == null || UidValidityTransactionListener.this.minUid < oldMax)
|
|
{
|
|
nodeService.setProperty(folderNodeRef, ImapModel.PROP_UIDVALIDITY, modifDate);
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("UIDVALIDITY was modified for folder, nodeRef:" + folderNodeRef);
|
|
}
|
|
}
|
|
if(UidValidityTransactionListener.this.maxUid != null)
|
|
{
|
|
nodeService.setProperty(folderNodeRef, ImapModel.PROP_MAXUID, UidValidityTransactionListener.this.maxUid);
|
|
if (logger.isDebugEnabled())
|
|
{
|
|
logger.debug("MAXUID was modified for folder, nodeRef:" + folderNodeRef);
|
|
}
|
|
}
|
|
}
|
|
nodeService.setProperty(folderNodeRef, ImapModel.PROP_CHANGE_TOKEN, changeToken);
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if provided nodeRef is in Sites/.../documentlibrary
|
|
*/
|
|
public boolean isNodeInSitesLibrary(NodeRef nodeRef)
|
|
{
|
|
boolean isInDocLibrary = false;
|
|
NodeRef parent = nodeService.getPrimaryParent(nodeRef).getParentRef();
|
|
while (parent != null && !nodeService.getType(parent).equals(SiteModel.TYPE_SITE))
|
|
{
|
|
String parentName = (String) nodeService.getProperty(parent, ContentModel.PROP_NAME);
|
|
if (parentName.equalsIgnoreCase("documentlibrary"))
|
|
{
|
|
isInDocLibrary = true;
|
|
}
|
|
nodeRef = parent;
|
|
if (nodeService.getPrimaryParent(nodeRef) != null)
|
|
{
|
|
parent = nodeService.getPrimaryParent(nodeRef).getParentRef();
|
|
}
|
|
}
|
|
if (parent == null)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return nodeService.getType(parent).equals(SiteModel.TYPE_SITE) && isInDocLibrary;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract attachments from a MimeMessage
|
|
*
|
|
* Puts the attachments into a subfolder below the parent folder.
|
|
*
|
|
* @return the node ref of the folder containing the attachments or null if there are no
|
|
* attachments.
|
|
*/
|
|
public NodeRef extractAttachments(
|
|
NodeRef parentFolder,
|
|
NodeRef messageFile,
|
|
MimeMessage originalMessage)
|
|
throws IOException, MessagingException
|
|
{
|
|
|
|
String messageName = (String)nodeService.getProperty(messageFile, ContentModel.PROP_NAME);
|
|
String attachmentsFolderName = messageName + "-attachments";
|
|
FileInfo attachmentsFolderFileInfo = null;
|
|
Object content = originalMessage.getContent();
|
|
if (content instanceof Multipart)
|
|
{
|
|
Multipart multipart = (Multipart) content;
|
|
|
|
for (int i = 0, n = multipart.getCount(); i < n; i++)
|
|
{
|
|
Part part = multipart.getBodyPart(i);
|
|
|
|
if ("attachment".equalsIgnoreCase(part.getDisposition()))
|
|
{
|
|
if (attachmentsFolderFileInfo == null)
|
|
{
|
|
attachmentsFolderFileInfo = fileFolderService.create(
|
|
parentFolder,
|
|
attachmentsFolderName,
|
|
ContentModel.TYPE_FOLDER);
|
|
nodeService.createAssociation(
|
|
messageFile,
|
|
attachmentsFolderFileInfo.getNodeRef(),
|
|
ImapModel.ASSOC_IMAP_ATTACHMENTS_FOLDER);
|
|
}
|
|
createAttachment(messageFile, attachmentsFolderFileInfo.getNodeRef(), part);
|
|
}
|
|
}
|
|
}
|
|
if(attachmentsFolderFileInfo != null)
|
|
{
|
|
return attachmentsFolderFileInfo.getNodeRef();
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an attachment given a mime part
|
|
*
|
|
* @param messageFile the file containing the message
|
|
* @param destinationFolder where to put the attachment
|
|
* @param part the mime part
|
|
*
|
|
* @throws MessagingException
|
|
* @throws IOException
|
|
*/
|
|
private void createAttachment(NodeRef messageFile, NodeRef destinationFolder, Part part) throws MessagingException, IOException
|
|
{
|
|
String fileName = part.getFileName();
|
|
try
|
|
{
|
|
fileName = MimeUtility.decodeText(fileName);
|
|
}
|
|
catch (UnsupportedEncodingException e)
|
|
{
|
|
if (logger.isWarnEnabled())
|
|
{
|
|
logger.warn("Cannot decode file name '" + fileName + "'", e);
|
|
}
|
|
}
|
|
|
|
ContentType contentType = new ContentType(part.getContentType());
|
|
|
|
if(contentType.getBaseType().equalsIgnoreCase("application/ms-tnef"))
|
|
{
|
|
// The content is TNEF
|
|
HMEFMessage hmef = new HMEFMessage(part.getInputStream());
|
|
|
|
//hmef.getBody();
|
|
List<org.apache.poi.hmef.Attachment> attachments = hmef.getAttachments();
|
|
for(org.apache.poi.hmef.Attachment attachment : attachments)
|
|
{
|
|
String subName = attachment.getLongFilename();
|
|
|
|
NodeRef attachmentNode = fileFolderService.searchSimple(destinationFolder, subName);
|
|
if (attachmentNode == null)
|
|
{
|
|
/*
|
|
* If the node with the given name does not already exist
|
|
* Create the content node to contain the attachment
|
|
*/
|
|
FileInfo createdFile = fileFolderService.create(
|
|
destinationFolder,
|
|
subName,
|
|
ContentModel.TYPE_CONTENT);
|
|
|
|
attachmentNode = createdFile.getNodeRef();
|
|
|
|
serviceRegistry.getNodeService().createAssociation(
|
|
messageFile,
|
|
attachmentNode,
|
|
ImapModel.ASSOC_IMAP_ATTACHMENT);
|
|
|
|
|
|
byte[] bytes = attachment.getContents();
|
|
ContentWriter writer = fileFolderService.getWriter(attachmentNode);
|
|
|
|
//TODO ENCODING - attachment.getAttribute(TNEFProperty.);
|
|
String extension = attachment.getExtension();
|
|
String mimetype = mimetypeService.getMimetype(extension);
|
|
if(mimetype != null)
|
|
{
|
|
writer.setMimetype(mimetype);
|
|
}
|
|
|
|
OutputStream os = writer.getContentOutputStream();
|
|
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
|
|
FileCopyUtils.copy(is, os);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// not TNEF
|
|
NodeRef attachmentNode = fileFolderService.searchSimple(destinationFolder, fileName);
|
|
if (attachmentNode == null)
|
|
{
|
|
/*
|
|
* If the node with the given name does not already exist
|
|
* Create the content node to contain the attachment
|
|
*/
|
|
FileInfo createdFile = fileFolderService.create(
|
|
destinationFolder,
|
|
fileName,
|
|
ContentModel.TYPE_CONTENT);
|
|
|
|
attachmentNode = createdFile.getNodeRef();
|
|
|
|
serviceRegistry.getNodeService().createAssociation(
|
|
messageFile,
|
|
attachmentNode,
|
|
ImapModel.ASSOC_IMAP_ATTACHMENT);
|
|
|
|
|
|
// the part is a normal IMAP attachment
|
|
ContentWriter writer = fileFolderService.getWriter(attachmentNode);
|
|
writer.setMimetype(contentType.getBaseType());
|
|
|
|
String charset = contentType.getParameter("charset");
|
|
if(charset != null)
|
|
{
|
|
writer.setEncoding(charset);
|
|
}
|
|
|
|
OutputStream os = writer.getContentOutputStream();
|
|
FileCopyUtils.copy(part.getInputStream(), os);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setNamespaceService(NamespaceService namespaceService)
|
|
{
|
|
this.namespaceService = namespaceService;
|
|
}
|
|
|
|
public NamespaceService getNamespaceService()
|
|
{
|
|
return namespaceService;
|
|
}
|
|
|
|
public void setSearchService(SearchService searchService)
|
|
{
|
|
this.searchService = searchService;
|
|
}
|
|
|
|
public SearchService getSearchService()
|
|
{
|
|
return searchService;
|
|
}
|
|
|
|
public void extractAttachments(NodeRef messageRef, MimeMessage originalMessage) throws IOException, MessagingException
|
|
{
|
|
attachmentsExtractor.extractAttachments(messageRef, originalMessage);
|
|
}
|
|
|
|
public String generateUniqueFilename(NodeRef destFolderNodeRef, String fileName)
|
|
{
|
|
if(fileFolderService.searchSimple(destFolderNodeRef, fileName) != null)
|
|
{
|
|
String name = fileName;
|
|
String ext = "";
|
|
if (fileName.lastIndexOf(".") != -1)
|
|
{
|
|
int index = fileName.lastIndexOf(".");
|
|
name = fileName.substring(0, index);
|
|
ext = fileName.substring(index);
|
|
}
|
|
int copyNum = 0;
|
|
do
|
|
{
|
|
copyNum++;
|
|
} while (fileFolderService.searchSimple(destFolderNodeRef, name + " (" + copyNum + ")" + ext) != null);
|
|
fileName = name + " (" + copyNum + ")" + ext;
|
|
}
|
|
|
|
return fileName;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |