diff --git a/config/alfresco/action-services-context.xml b/config/alfresco/action-services-context.xml index 2a62c14ef5..2e40e81e60 100644 --- a/config/alfresco/action-services-context.xml +++ b/config/alfresco/action-services-context.xml @@ -473,6 +473,9 @@ /${spaces.company_home.childname} + + ${web.application.context.url} + diff --git a/config/alfresco/application-context.xml b/config/alfresco/application-context.xml index 684b0494a1..e6d122e352 100644 --- a/config/alfresco/application-context.xml +++ b/config/alfresco/application-context.xml @@ -52,6 +52,7 @@ + diff --git a/config/alfresco/bootstrap/spaces.xml b/config/alfresco/bootstrap/spaces.xml index b7c16ab94a..13adb72acd 100644 --- a/config/alfresco/bootstrap/spaces.xml +++ b/config/alfresco/bootstrap/spaces.xml @@ -1,6 +1,7 @@ + xmlns:app="http://www.alfresco.org/model/application/1.0" + xmlns:emailserver="http://www.alfresco.org/model/emailserver/1.0"> @@ -24,34 +25,34 @@ Consumer - - ${spaces.dictionary.name} - space-icon-default - ${spaces.dictionary.name} - ${spaces.dictionary.description} - - - - ${spaces.templates.name} - space-icon-default - ${spaces.templates.name} - ${spaces.templates.description} - - - - ${spaces.templates.content.name} - space-icon-default - ${spaces.templates.content.name} - ${spaces.templates.content.description} - - - - ${spaces.templates.email.name} - space-icon-default - ${spaces.templates.email.name} - ${spaces.templates.email.description} + + ${spaces.dictionary.name} + space-icon-default + ${spaces.dictionary.name} + ${spaces.dictionary.description} + + + + ${spaces.templates.name} + space-icon-default + ${spaces.templates.name} + ${spaces.templates.description} + + + + ${spaces.templates.content.name} + space-icon-default + ${spaces.templates.content.name} + ${spaces.templates.content.description} + + + + ${spaces.templates.email.name} + space-icon-default + ${spaces.templates.email.name} + ${spaces.templates.email.description} @@ -80,69 +81,68 @@ - - - - - ${alfresco_user_store.guestusername} - Consumer - - - - ${spaces.templates.rss.name} - space-icon-default - ${spaces.templates.rss.name} - ${spaces.templates.rss.description} - - - - - GROUP_EVERYONE - Contributor - - - - ${spaces.savedsearches.name} - space-icon-default - ${spaces.savedsearches.name} - ${spaces.savedsearches.description} - - - - ${spaces.scripts.name} - space-icon-default - ${spaces.scripts.name} - ${spaces.scripts.description} - - - - - - - ${alfresco_user_store.guestusername} - Consumer - - - GROUP_EVERYONE - Consumer - - - - ${spaces.guest_home.name} - space-icon-default - ${spaces.guest_home.name} - ${spaces.guest_home.description} - - - - ${spaces.user_homes.name} - space-icon-default - ${spaces.user_homes.name} - ${spaces.user_homes.description} - - - - + + + + + ${alfresco_user_store.guestusername} + Consumer + + + + ${spaces.templates.rss.name} + space-icon-default + ${spaces.templates.rss.name} + ${spaces.templates.rss.description} + + + + + GROUP_EVERYONE + Contributor + + + + ${spaces.savedsearches.name} + space-icon-default + ${spaces.savedsearches.name} + ${spaces.savedsearches.description} + + + + ${spaces.scripts.name} + space-icon-default + ${spaces.scripts.name} + ${spaces.scripts.description} + + + + + + + ${alfresco_user_store.guestusername} + Consumer + + + GROUP_EVERYONE + Consumer + + + + ${spaces.guest_home.name} + space-icon-default + ${spaces.guest_home.name} + ${spaces.guest_home.description} + + + + ${spaces.user_homes.name} + space-icon-default + ${spaces.user_homes.name} + ${spaces.user_homes.description} + + + \ No newline at end of file diff --git a/config/alfresco/bootstrap/system.xml b/config/alfresco/bootstrap/system.xml index 2e8e23bb79..95a9200224 100644 --- a/config/alfresco/bootstrap/system.xml +++ b/config/alfresco/bootstrap/system.xml @@ -49,7 +49,7 @@ ${alfresco_user_store.adminusername} Administrator - + admin@alfresco.com /${spaces.company_home.childname} bootstrapHomeFolderProvider diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index d16fe3cf04..38d4842c04 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -928,6 +928,7 @@ alfresco/model/bpmModel.xml alfresco/model/wcmModel.xml alfresco/model/forumModel.xml + alfresco/model/imapModel.xml alfresco/model/applicationModel.xml diff --git a/config/alfresco/extension/custom-imap-server-context.xml.sample b/config/alfresco/extension/custom-imap-server-context.xml.sample new file mode 100755 index 0000000000..a706326c12 --- /dev/null +++ b/config/alfresco/extension/custom-imap-server-context.xml.sample @@ -0,0 +1,19 @@ + + + + + + + + true + + + + classpath:alfresco/imap-server.properties + + classpath:alfresco/extension/custom-imap-server.properties + + + + + diff --git a/config/alfresco/extension/custom-imap-server.properties.sample b/config/alfresco/extension/custom-imap-server.properties.sample new file mode 100755 index 0000000000..c87cdff75c --- /dev/null +++ b/config/alfresco/extension/custom-imap-server.properties.sample @@ -0,0 +1,2 @@ +imap.server.enabled=true +imap.server.port=143 diff --git a/config/alfresco/extension/imap-bootsrap-context.xml.sample b/config/alfresco/extension/imap-bootsrap-context.xml.sample new file mode 100755 index 0000000000..307e8f2996 --- /dev/null +++ b/config/alfresco/extension/imap-bootsrap-context.xml.sample @@ -0,0 +1,40 @@ + + + + + + patch.imapFolders + patch.imapFolders.description + 0 + ${version.schema} + 10000 + + + + + + + + + + alfresco/templates/imap/imap_config_space.acp + alfresco/templates/imap/email_actions_space.acp + alfresco/templates/imap/command_processor_scripts.acp + + + + patch.imapUserFolders + patch.imapUserFolders.description + 0 + ${version.schema} + 10000 + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/imap-config.xml b/config/alfresco/imap-config.xml new file mode 100755 index 0000000000..6ae9ec65fd --- /dev/null +++ b/config/alfresco/imap-config.xml @@ -0,0 +1,14 @@ + + + + + workspace://SpacesStore + /app:company_home + + + workspace://SpacesStore + /app:company_home + + + + \ No newline at end of file diff --git a/config/alfresco/imap-server-context.xml b/config/alfresco/imap-server-context.xml new file mode 100755 index 0000000000..88259243d2 --- /dev/null +++ b/config/alfresco/imap-server-context.xml @@ -0,0 +1,91 @@ + + + + + + + + + alfresco.messages.imap-service + + + + + + + true + + + + classpath:alfresco/imap-server.properties + + + + + + + + + + + + + + + + + + + + + + ${mail.from.default} + + + + ${web.application.context.url} + + + + ${spaces.store}/${spaces.company_home.childname}/${spaces.imap_home.childname} + + + + ${spaces.store}/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.imapConfig.childname}/${spaces.imap_templates.childname} + + + + + + ${imap.server.port} + + + ${imap.server.enabled} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/imap-server.properties b/config/alfresco/imap-server.properties new file mode 100755 index 0000000000..42d301e12e --- /dev/null +++ b/config/alfresco/imap-server.properties @@ -0,0 +1,4 @@ +imap.server.enabled=false +imap.server.port=143 + +#imap.server.web.application.context.url=http://localhost:8080/alfresco diff --git a/config/alfresco/imap/scripts/command-processor.js b/config/alfresco/imap/scripts/command-processor.js new file mode 100755 index 0000000000..963f147c10 --- /dev/null +++ b/config/alfresco/imap/scripts/command-processor.js @@ -0,0 +1,77 @@ + + +function processCommand() +{ + var isEmailed = document.hasAspect("emailserver:emailed"); + + logger.log("Command Processor: isEmailed=" + isEmailed); + + if (isEmailed) + { + // Delete email attachments + var attachments = document.assocs["attachments"]; + if (attachments != null) + { + for (var i = 0; i < attachments.length; i++) + { + attachments[i].remove(); + } + } + + var command = document.properties["cm:title"]; + logger.log("Command Processor: command=" + command); + + var parts = new Array(); + var str = command; + var i = 0; + while (true) + { + var index = str.indexOf("-"); + if (index == -1) + { + parts[i] = str; + break; + } + parts[i] = str.substring(0, index); + str = str.substr(index + 1); + i++; + } + + + + // do---...- + if (parts.length < 3 || parts[0].toLowerCase() != "do") + { + var message = "Unknown command: " + command; + logger.log(message); + createEmail(message, message, message, false); + return; + } + + var commandName = parts[1].toLowerCase(); + var commandFolder = space.childByNamePath(commandName); + logger.log("Found '" + commandName + "' command folder: '" + commandFolder + "'"); + if (commandFolder == null) + { + var message = "Command Processor: wrong command=" + command; + createEmail(message, message, message, false); + logger.log(message); + return; + } + + document.move(commandFolder); + + } + +} + +processCommand(); + + + + + + + + + diff --git a/config/alfresco/imap/scripts/command-search.js b/config/alfresco/imap/scripts/command-search.js new file mode 100755 index 0000000000..5ffecaa983 --- /dev/null +++ b/config/alfresco/imap/scripts/command-search.js @@ -0,0 +1,243 @@ + + +/** + * Resources + */ + +/* var templatePath = "/Data Dictionary/Imap Configs/Templates/imap_search_response_text_html.ftl";*/ +var errorParameter = "Error: The query parameter is not set!"; +var errorXPathNotValid = "Error: The Xpath query is not valid."; +var unknownCommand = "Unknown command"; + +/** + * Globals + */ +var title; +var command; + +/** + * Create content for e-mail in text format + * + * @nodes (Array) ScriptNodes array + * @return content for e-mail in text format + */ +function createContentTextPlain(nodes) +{ + var content = "Command: " + title + "\n\n"; + for (var i = 0; i < nodes.length; i++) + { + content = content + "Name: " + nodes[i].getName() + "\nUrl: " + + webApplicationContextUrl + nodes[i].getUrl(); + + if (nodes[i].isDocument) + { + content = content + "\nDownload Url: " + + webApplicationContextUrl + nodes[i].getDownloadUrl(); + } + + content = content + "\n\n"; + } + return content; +} + +/** + * This for possible processing. It need to be investigated. + * The possible solution is to send a search request into FreeMarker template and let the template do search! + * @param nodes + * @return + */ +function createResponseTextHtml(nodes) +{ + var template = companyhome.childByNamePath(templatePath); + var result; + if (template != null) + { + var args = new Array(); + args["title"] = title; + args["nodes"] = nodes; /*it does not work; need to investigate how to send this to freemarker processing*/ + args["webApplicationContextUrl"] = webApplicationContextUrl; + result = document.processTemplate(template, args); + logger.log("Response template is found. Response body is created using template."); + } + else + { + result = createContentTextHtml(nodes); + logger.log("Response template is NOT found. Response is created using default function."); + } + return result; +} + +/** + * Create content for e-mail in html format + * + * @nodes (Array) ScriptNodes array + * @return content for e-mail in html format + */ +function createContentTextHtml(nodes) +{ + var content = "Command: " + title + "\n

\n"; + content += "\n"; + content += ""; + content += ""; + content += ""; + content += ""; + content += ""; + content += ""; + content += "\n" + + + for (var i = 0; i < nodes.length; i++) + { + content += "\n"; + content += ""; + content += ""; + content += "\n"; + content += "\n"; + } + content += "
NameUrlDownload Url
" + nodes[i].getName() + "" + + webApplicationContextUrl + nodes[i].getUrl() + " "; + if (nodes[i].isDocument) + { + content += "" + + webApplicationContextUrl + nodes[i].getDownloadUrl() + ""; + } + content += "
"; + return content; +} + +/** + * Execute search command + * + * @params (string) command parameters + */ +function commandSearch(params) +{ + var store = "workspace://SpacesStore"; + var query; + var subject = "Search result"; + var type = "lucene"; + var paramArray = params.split(";"); + for (var i = 0; i < paramArray.length; i++) + { + var param = paramArray[i].split("="); + param[0] = param[0].toLowerCase(); + + switch (param[0]) + { + case "store": store = param[1]; break; + case "query": query = param[1]; break; + case "subject": subject = param[1]; break; + case "type": type = param[1].toLowerCase(); break; + } + } + + if (query == null) + { + createEmail(errorParameter, errorParameter, errorParameter, false); + return; + } + + var nodes; + + try + { + switch (type) + { + case "lucene": + nodes = search.luceneSearch(store, query); + break; + case "xpath": + var isValid = search.isValidXpathQuery(query); + if (isValid == true) + { + nodes = search.xpathSearch(store, query); + } + else + { + createEmail(errorXPathNotValid, errorXPathNotValid, errorXPathNotValid, false); + return; + } + break; + case "node": + var node = search.findNode(query); + if (node == null) break; + nodes = new Array(node); + break; + case "tag": + nodes = search.tagSearch(store, query); + break; + } + } + catch (exception) + { + createEmail(exception.message, exception.message, "Search Error"); + return; + } + + if (nodes == null || nodes.length == 0) + { + var message = "Nothing was found using query: '" + subject + "'."; + createEmail(message, message, subject); + return; + } + /*createEmail(createResponseTextHtml(nodes), createContentTextPlain(nodes), subject, true);*/ + createEmail(createContentTextHtml(nodes), createContentTextPlain(nodes), subject, false); +} +/** + * Decode subject + * + * @subject (string) subject + */ +function decodeSubject(subject) +{ + var s = new Array(); + s[0] = new Array("\\", "%5c"); + s[1] = new Array("/", "%2f"); + s[2] = new Array("*", "%2a"); + s[3] = new Array("|", "%7c"); + s[4] = new Array(":", "%3a"); + s[5] = new Array("\"", "%22"); + s[6] = new Array("<", "%3c"); + s[7] = new Array(">", "%3e"); + s[8] = new Array("?", "%3f"); + + for (var i = 0; i < s.length; i++) + { + var re = new RegExp(s[i][1], 'g'); + subject = subject.replace(re, s[i][0]); + } + + return subject; +} + + +function main() +{ + title = decodeSubject(document.properties["cm:title"]); + command = title.split("-"); + if (command[0].toLowerCase() == "do") + { + if (command[1].toLowerCase() == "search") + { + commandSearch(title.substring(4 + command[1].length)); + } + else + { + var message = unknownCommand + ": '" + title + "'"; + createEmail(message, message, message, false); + } + } + else + { + var message = unknownCommand + ": '" + title + "'"; + createEmail(message, message, message, false); + } + + document.remove(); +} + + +logger.log("Start search command."); +main(); +logger.log("End search command."); + diff --git a/config/alfresco/imap/scripts/command-utils.js b/config/alfresco/imap/scripts/command-utils.js new file mode 100755 index 0000000000..64039946f5 --- /dev/null +++ b/config/alfresco/imap/scripts/command-utils.js @@ -0,0 +1,56 @@ +/** +* Create e-mail +* contentTextHtml (string) html content +* contentTextPlain (string) text content +*/ +function createEmail(contentTextHtml, contentTextPlain, subject, templateUsed) +{ + var command = document.properties["cm:title"]; + var userName = person.properties["cm:userName"]; + + var inboxFolder = companyhome.childByNamePath("IMAP Home/" + userName + "/INBOX"); + if (inboxFolder == null) + { + logger.log("Command Processor: INBOX folder does't exists."); + return; + } + + var nextMessageUID = inboxFolder.properties["imap:nextMessageUID"]; + inboxFolder.properties["imap:nextMessageUID"] = nextMessageUID + 1; + inboxFolder.save(); + + var response = inboxFolder.createNode("response" + Date.now(), "imap:imapContent"); + response.properties["imap:messageFrom"] = "command@alfresco.com"; + response.properties["imap:messageSubject"] = subject; + response.properties["imap:messageTo"] = document.properties["cm:originator"]; + response.properties["imap:messageCc"] = ""; + response.properties["imap:messageUID"] = nextMessageUID; + + response.save(); + + var textBody = response.createNode("Body.txt", "imap:imapBody"); + textBody.content = contentTextPlain; + textBody.save(); + + var htmlBody = response.createNode("Body.html", "imap:imapBody"); + if (templateUsed == true) + { + htmlBody.content = contentTextHtml; + } + else + { + htmlBody.content = "" + + "" + + "" + + "" + + "" + + "" + + "
" + contentTextHtml + "
"; + } + htmlBody.save(); + +} \ No newline at end of file diff --git a/config/alfresco/import-export-context.xml b/config/alfresco/import-export-context.xml index 666cdb7638..e98746534b 100644 --- a/config/alfresco/import-export-context.xml +++ b/config/alfresco/import-export-context.xml @@ -326,6 +326,9 @@ ${spaces.user_homes.childname} ${spaces.sites.childname} ${spaces.templates.email.invite.childname} + ${spaces.imap_home.childname} + ${spaces.imapConfig.childname} + ${spaces.imap_templates.childname}
@@ -516,6 +519,7 @@ alfresco/bootstrap/sitesSpace.xml alfresco/messages/bootstrap-spaces + diff --git a/config/alfresco/messages/bootstrap-imapScripts.properties b/config/alfresco/messages/bootstrap-imapScripts.properties new file mode 100755 index 0000000000..39e505cdfd --- /dev/null +++ b/config/alfresco/messages/bootstrap-imapScripts.properties @@ -0,0 +1,12 @@ +imap.command_processor.name=command-processor.js +imap.command_processor.title=Command Processor +imap.command_processor.description=Email Command Processor Script + +imap.command_search.name=command-search.js +imap.command_search.title=Search Command +imap.command_search.description=Email Search Command Script + +imap.command_utils.name=command-utils.js +imap.command_utils.title=Command Utils +imap.command_utils.description=Email Command Utils + \ No newline at end of file diff --git a/config/alfresco/messages/bootstrap-spaces.properties b/config/alfresco/messages/bootstrap-spaces.properties index ca6aa301e9..dabd16aa08 100644 --- a/config/alfresco/messages/bootstrap-spaces.properties +++ b/config/alfresco/messages/bootstrap-spaces.properties @@ -6,6 +6,12 @@ spaces.company_home.description=The company root space spaces.dictionary.name=Data Dictionary spaces.dictionary.description=User managed definitions +spaces.imapConfig.name=Imap Configs +spaces.imapConfig.description=Imap Configs + +spaces.imap_templates.name=Templates +spaces.imap_templates.description=Templates for IMAP generated messages + spaces.templates.name=Space Templates spaces.templates.description=Space folder templates @@ -44,3 +50,6 @@ spaces.sites.description=Site Collaboration Spaces spaces.templates.email.invite.name=invite spaces.templates.email.invite.description=Invite email templates + +spaces.imap_home.name=IMAP Home +spaces.imap_home.description=IMAP Home \ No newline at end of file diff --git a/config/alfresco/messages/imap-service.properties b/config/alfresco/messages/imap-service.properties new file mode 100755 index 0000000000..fdfd12856a --- /dev/null +++ b/config/alfresco/messages/imap-service.properties @@ -0,0 +1,10 @@ +# +# Imap I18N messages +# + +# Information messages. prefix 'imap.server.info' +imap.server.info.message_body_not_found = "The message body parts are not found." + +# Error messages. prefix 'imap.server.error' +imap.server.error.properties_dont_exist = "Appropriate properties do not exist." + diff --git a/config/alfresco/messages/patch-service.properties b/config/alfresco/messages/patch-service.properties index 2a3253d9f1..28de79ac2a 100644 --- a/config/alfresco/messages/patch-service.properties +++ b/config/alfresco/messages/patch-service.properties @@ -1,4 +1,4 @@ -# PatchService messages +# PatchService messages patch.service.preceeded_by_alternative=Preceeded by alternative patch ''{0}''. patch.service.not_relevant=Not relevant to schema {0} patch.executer.checking=Checking for patches to apply ... @@ -261,3 +261,11 @@ patch.mtShareExistingTenants.result=Update existing tenants for MT Share. patch.mtShareExistingTenants.result.not_applicable=Patch applied, although no changes made since MT is not enabled. patch.redeployInvitationProcess.description=Re-deploy Invitation Process Definitions. + +patch.imapFolders.description=Creates folders tree necessary for IMAP functionality +patch.imapFolders.result.exists=The 'Imap Configs' folder already exists +patch.imapFolders.result.created=The 'Imap Configs' folder was successfully created + +patch.imapUserFolders.description=Creates folders tree necessary for IMAP functionality +patch.imapUserFolders.result.exists=The 'IMAP Home' folder already exists +patch.imapUserFolders.result.created=The 'IMAP Home' folder was successfully created diff --git a/config/alfresco/model/imapModel.xml b/config/alfresco/model/imapModel.xml new file mode 100755 index 0000000000..ee93e810b2 --- /dev/null +++ b/config/alfresco/model/imapModel.xml @@ -0,0 +1,100 @@ + + + IMAP Content Model + Alfresco + 2009-01-20 + 1.0 + + + + + + + + + + + + + IMAP Folder + cm:folder + + + d:boolean + + + d:boolean + true + true + + + + + + IMAP File + cm:folder + + + + d:text + + + d:text + + + d:text + + + d:text + + + + imap:flaggable + + + + + Attachment to the IMAP message + cm:content + + + d:text + + + + + + Body of the IMAP message + cm:content + + + + + + + + d:boolean + + + d:boolean + + + d:boolean + + + d:boolean + + + d:boolean + + + d:boolean + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 285650cd52..80d75769da 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -6,6 +6,8 @@ repository.name=Main Repository dir.root=./alf_data +web.application.context.url=http://localhost:8080/alfresco + dir.contentstore=${dir.root}/contentstore dir.contentstore.deleted=${dir.root}/contentstore.deleted @@ -226,6 +228,8 @@ spaces.company_home.childname=app:company_home spaces.guest_home.childname=app:guest_home spaces.dictionary.childname=app:dictionary spaces.templates.childname=app:space_templates +spaces.imapConfig.childname=app:imap_configs +spaces.imap_templates.childname=app:imap_templates spaces.templates.content.childname=app:content_templates spaces.templates.email.childname=app:email_templates spaces.templates.rss.childname=app:rss_templates @@ -235,6 +239,7 @@ spaces.wcm.childname=app:wcm spaces.wcm_content_forms.childname=app:wcm_forms spaces.content_forms.childname=app:forms spaces.user_homes.childname=app:user_homes +spaces.imap_home.childname=imap:imap_home spaces.sites.childname=st:sites spaces.templates.email.invite.childname=cm:invite diff --git a/config/alfresco/templates/imap/command_processor_scripts.acp b/config/alfresco/templates/imap/command_processor_scripts.acp new file mode 100755 index 0000000000..dda4f430b3 Binary files /dev/null and b/config/alfresco/templates/imap/command_processor_scripts.acp differ diff --git a/config/alfresco/templates/imap/email_actions_space.acp b/config/alfresco/templates/imap/email_actions_space.acp new file mode 100755 index 0000000000..f54d9069c0 Binary files /dev/null and b/config/alfresco/templates/imap/email_actions_space.acp differ diff --git a/config/alfresco/templates/imap/imap_config_space.acp b/config/alfresco/templates/imap/imap_config_space.acp new file mode 100755 index 0000000000..d2dee2323c Binary files /dev/null and b/config/alfresco/templates/imap/imap_config_space.acp differ diff --git a/config/alfresco/templates/imap/imap_message_text_html.ftl b/config/alfresco/templates/imap/imap_message_text_html.ftl new file mode 100755 index 0000000000..5af0c44148 --- /dev/null +++ b/config/alfresco/templates/imap/imap_message_text_html.ftl @@ -0,0 +1,173 @@ + + + + + + + + + + +
+

Document (name): ${document.name}

+
+
+ Metadata + + <#if document.properties.title?exists> + + <#else> + + + <#if document.properties.description?exists> + + <#else> + + + + + + + +
Title:${document.properties.title}
Title: 
Description:${document.properties.description}
Description: 
Creator:${document.properties.creator}
Created:${document.properties.created?datetime}
Modifier:${document.properties.modifier}
Modified:${document.properties.modified?datetime}
Size:${document.size / 1024} Kb
+
+
+ Content links + + + + + + + + + + + + + + +
+
+ Start Workflow +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Workflow type: + + +
Asign to: + +
Priority: + +
Due date: + Day: +   + Month:   + Year: +
Description:
+
+
+ + + \ No newline at end of file diff --git a/config/alfresco/templates/imap/imap_message_text_plain.ftl b/config/alfresco/templates/imap/imap_message_text_plain.ftl new file mode 100755 index 0000000000..b8edc2195e --- /dev/null +++ b/config/alfresco/templates/imap/imap_message_text_plain.ftl @@ -0,0 +1,35 @@ +------------------------------------------------------------------------------ +Document name: ${document.name} +------------------------------------------------------------------------------ + + <#if document.properties.title?exists> +Title: ${document.properties.title} + <#else> +Title: NONE + + <#if document.properties.description?exists> +Description: ${document.properties.description} + <#else> +Description: NONE + +Creator: ${document.properties.creator} +Created: ${document.properties.created?datetime} +Modifier: ${document.properties.modifier} +Modified: ${document.properties.modified?datetime} +Size: ${document.size / 1024} Kb + + +CONTENT LINKS + +Content folder: ${contextUrl}${document.displayPath} +Content URL: ${contextUrl}${document.url} +Download URL: ${contextUrl}${document.downloadUrl} +WebDAV URL: ${contextUrl}${document.webdavUrl} + +START WORKFLOW + +{It is not possible to create a customizible workflow in txt format! + It is possible to create static links to IMAP Workflow Handler webscript, + But, in this case all parameters must be hardcoded in the link. + See http://localhost:8080/alfresco/service/description/org/alfresco/imap/start-workflow.get + for usage information.} \ No newline at end of file diff --git a/config/alfresco/templates/imap/template_test.html b/config/alfresco/templates/imap/template_test.html new file mode 100755 index 0000000000..21a5a5d5e4 --- /dev/null +++ b/config/alfresco/templates/imap/template_test.html @@ -0,0 +1,157 @@ + + + + + + + + + + +
+

Document (name): ${document.name}

+
+
+ Metadata + + <#if document.properties.title?exists> + + <#else> + + + <#if document.properties.description?exists> + + <#else> + + + + + + + +
Title:${document.properties.title}
Title: 
Description:${document.properties.description}
Description: 
Creator:${document.properties.creator}
Created:${document.properties.created?datetime}
Modifier:${document.properties.modifier}
Modified:${document.properties.modified?datetime}
Size:${document.size / 1024} Kb
+
+
+ Content links + + + + + + + + + + + + + + +
+
+ Start Workflow +
+ + + + + + + + + + + + + + + + + + + + + + + +
Workflow type: + + +
Asign to: + +
Priority: + +
Due date:
Description:
+
+
+ + + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/imap/imap-enabled.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/imap/imap-enabled.get.desc.xml new file mode 100755 index 0000000000..c405507342 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/imap/imap-enabled.get.desc.xml @@ -0,0 +1,11 @@ + + IMAP Server Status + + This script return status of IMAP server (enabled/diabled) + + /imap/servstatus + extension + user + required + IMAP + diff --git a/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.desc.xml new file mode 100755 index 0000000000..d8849e0ff6 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.desc.xml @@ -0,0 +1,12 @@ + + IMAP Workflow Handler + + This webscript starts a different workflows. It have used in the IMAP email body links. + (The optional feature is the reply email with a report.) + + /imap/start-workflow?alfTicket={ticket}&nodeRefId={id}&workflowType={wt}&assignTo={at}&workflowDueDateDay={ddd}&workflowDueDateMonth={ddm}&workflowDueDateYear={ddy}&description={desc} + extension + user + required + IMAP + diff --git a/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.html.ftl b/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.html.ftl new file mode 100755 index 0000000000..3b2b45b169 --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.html.ftl @@ -0,0 +1,71 @@ + + + + + + + + + + +

Workflow has started successfully.

+ + + + + + + + + + + + + + + + + + + + + + +
Ticket:${args.alfTicket}
NodeRef id:${args.nodeRefId}
Workflow type:${args.workflowType}
Asign to:${args.assignTo}
Due date:${args.workflowDueDateDay}/${args.workflowDueDateMonth}/${args.workflowDueDateYear}
Description:${args.description}
Result: ---
+ + \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.js b/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.js new file mode 100755 index 0000000000..c55922ffaf --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/imap/start-workflow.get.js @@ -0,0 +1,42 @@ +function main() +{ + logger.log("Start workflow form 'Start workflow' webscript"); + var docNode = search.findNode("workspace://SpacesStore/" + args.nodeRefId); + if (docNode == undefined) + { + status.code = 404; + status.message = "Content with NodeRef id '" + args.nodeRefId + "' not found."; + status.redirect = true; + return; + } + var workflowType = "jbpm$wf:" + args.workflowType; + var assignTo = people.getPerson(args.assignTo); + if (assignTo == undefined) + { + status.code = 404; + status.message = "Person with username '" + args.assignTo + "' not found."; + status.redirect = true; + return; + } + var day = args.workflowDueDateDay; + var month = args.workflowDueDateMonth; + var year = args.workflowDueDateYear; + if (year != null && year.length == 2) + { + year = "20" + year; + } + var dueDate = new Date(year, month - 1, day); + var description = args.description; + + var workflow = actions.create("start-workflow"); + workflow.parameters.workflowName = workflowType; + workflow.parameters["bpm:workflowDescription"] = description; + workflow.parameters["bpm:assignee"] = assignTo; + workflow.parameters["bpm:workflowPriority"] = args.workflowPriority; + if (dueDate != null) + { + workflow.parameters["bpm:workflowDueDate"] = dueDate; + } + workflow.execute(docNode); +} +main(); \ No newline at end of file diff --git a/source/java/org/alfresco/email/server/handler/FolderEmailMessageHandler.java b/source/java/org/alfresco/email/server/handler/FolderEmailMessageHandler.java index af0b951ec8..152d302634 100644 --- a/source/java/org/alfresco/email/server/handler/FolderEmailMessageHandler.java +++ b/source/java/org/alfresco/email/server/handler/FolderEmailMessageHandler.java @@ -40,6 +40,8 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.service.cmr.email.EmailMessage; import org.alfresco.service.cmr.email.EmailMessageException; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -120,24 +122,54 @@ public class FolderEmailMessageHandler extends AbstractEmailMessageHandler // Write the message content if (message.getBody() != null) { - InputStream contentIs = message.getBody().getContent(); - // The message body is plain text, unless an extension has been provided - MimetypeService mimetypeService = getMimetypeService(); - String mimetype = mimetypeService.guessMimetype(messageSubject); - if (mimetype.equals(MimetypeMap.MIMETYPE_BINARY)) + if (message.getBody().getSize() == -1) { - mimetype= MimetypeMap.MIMETYPE_TEXT_PLAIN; + // If message body is empty we write space as a content + // to make possible rule processing + // (Rules don't work on empty documents) + writeSpace(contentNodeRef); + } + else + { + InputStream contentIs = message.getBody().getContent(); + // The message body is plain text, unless an extension has been provided + MimetypeService mimetypeService = getMimetypeService(); + String mimetype = mimetypeService.guessMimetype(messageSubject); + if (mimetype.equals(MimetypeMap.MIMETYPE_BINARY)) + { + mimetype = MimetypeMap.MIMETYPE_TEXT_PLAIN; + } + // Use the default encoding. It will get overridden if the body is text. + String encoding = message.getBody().getEncoding(); + + writeContent(contentNodeRef, contentIs, mimetype, encoding); } - // Use the default encoding. It will get overridden if the body is text. - String encoding = message.getBody().getEncoding(); - - writeContent(contentNodeRef, contentIs, mimetype, encoding); } // Add attachments addAttachments(spaceNodeRef, contentNodeRef, message); } + /** + * This method writes space as a content. We need this space because rules doesn't proceed documents with empty content. We need rule processing for command email messages with + * empty body. + * + * @param nodeRef Reference to the parent node + */ + private void writeSpace(NodeRef nodeRef) + { + if (log.isDebugEnabled()) + { + log.debug("Write space string"); + } + + ContentService contentService = getContentService(); + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(" "); + } + /** * Adds titled aspect to the specified node. * diff --git a/source/java/org/alfresco/email/server/impl/subetha/SubethaEmailMessage.java b/source/java/org/alfresco/email/server/impl/subetha/SubethaEmailMessage.java index 4bc026ac23..91c344c1a5 100644 --- a/source/java/org/alfresco/email/server/impl/subetha/SubethaEmailMessage.java +++ b/source/java/org/alfresco/email/server/impl/subetha/SubethaEmailMessage.java @@ -153,7 +153,7 @@ public class SubethaEmailMessage implements EmailMessage try { - subject = mimeMessage.getSubject(); + subject = encodeSubject(mimeMessage.getSubject()); } catch (MessagingException e) { @@ -373,7 +373,8 @@ public class SubethaEmailMessage implements EmailMessage for (EmailMessagePart attachment : attachments) { - if (attachment instanceof SubethaEmailMessagePart) { + if (attachment instanceof SubethaEmailMessagePart) + { ((SubethaEmailMessagePart) attachment).setRmiRegistry(rmiRegistryHost, rmiRegistryPort); } } @@ -410,4 +411,24 @@ public class SubethaEmailMessage implements EmailMessage return attachments; } + /** + * Replaces characters \/*|:"<>? on their hex values. Subject field is used as name of the content, so we need to replace characters that are forbidden in content names. + * + * @param subject String representing subject + * @return Encoded string + */ + static private String encodeSubject(String subject) + { + String result = subject.trim(); + String[][] s = new String[][] { { "\\", "%5c" }, { "/", "%2f" }, { "*", "%2a" }, { "|", "%7c" }, { ":", "%3a" }, { "\"", "%22" }, { "<", "%3c" }, { ">", "%3e" }, + { "?", "%3f" } }; + + for (int i = 0; i < s.length; i++) + { + result = result.replace(s[i][0], s[i][1]); + } + + return result; + } + } diff --git a/source/java/org/alfresco/model/ImapModel.java b/source/java/org/alfresco/model/ImapModel.java new file mode 100755 index 0000000000..f918e53452 --- /dev/null +++ b/source/java/org/alfresco/model/ImapModel.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.model; + +import org.alfresco.service.namespace.QName; + +/** + * IMAP Model Constants + * + * @author Mike Shavnev + */ +public interface ImapModel +{ + static final String IMAP_MODEL_1_0_URI = "http://www.alfresco.org/model/imap/1.0"; + + static final QName ASPECT_IMAP_FOLDER_SUBSCRIBED = QName.createQName(IMAP_MODEL_1_0_URI, "subscribed"); + static final QName ASPECT_IMAP_FOLDER_NONSELECTABLE = QName.createQName(IMAP_MODEL_1_0_URI, "nonselectable"); + + static final QName TYPE_IMAP_CONTENT = QName.createQName(IMAP_MODEL_1_0_URI, "imapContent"); + static final QName PROP_MESSAGE_FROM = QName.createQName(IMAP_MODEL_1_0_URI, "messageFrom"); + static final QName PROP_MESSAGE_TO = QName.createQName(IMAP_MODEL_1_0_URI, "messageTo"); + static final QName PROP_MESSAGE_CC = QName.createQName(IMAP_MODEL_1_0_URI, "messageCc"); + static final QName PROP_MESSAGE_SUBJECT = QName.createQName(IMAP_MODEL_1_0_URI, "messageSubject"); + + static final QName ASPECT_FLAGGABLE = QName.createQName(IMAP_MODEL_1_0_URI, "flaggable"); + static final QName PROP_FLAG_ANSWERED = QName.createQName(IMAP_MODEL_1_0_URI, "flagAnswered"); + static final QName PROP_FLAG_DELETED = QName.createQName(IMAP_MODEL_1_0_URI, "flagDeleted"); + static final QName PROP_FLAG_DRAFT = QName.createQName(IMAP_MODEL_1_0_URI, "flagDraft"); + static final QName PROP_FLAG_SEEN = QName.createQName(IMAP_MODEL_1_0_URI, "flagSeen"); + static final QName PROP_FLAG_RECENT = QName.createQName(IMAP_MODEL_1_0_URI, "flagRecent"); + static final QName PROP_FLAG_FLAGGED = QName.createQName(IMAP_MODEL_1_0_URI, "flagFlagged"); + + static final QName TYPE_IMAP_BODY = QName.createQName(IMAP_MODEL_1_0_URI, "imapBody"); + + static final QName TYPE_IMAP_ATTACH = QName.createQName(IMAP_MODEL_1_0_URI, "imapAttach"); + static final QName PROP_ATTACH_ID = QName.createQName(IMAP_MODEL_1_0_URI, "attachID"); + +} diff --git a/source/java/org/alfresco/repo/action/executer/ScriptActionExecuter.java b/source/java/org/alfresco/repo/action/executer/ScriptActionExecuter.java index 29fef29d57..e82dd044a2 100644 --- a/source/java/org/alfresco/repo/action/executer/ScriptActionExecuter.java +++ b/source/java/org/alfresco/repo/action/executer/ScriptActionExecuter.java @@ -58,7 +58,8 @@ public class ScriptActionExecuter extends ActionExecuterAbstractBase private String companyHomePath; private StoreRef storeRef; private ScriptLocation scriptLocation; - + private String webApplicationContextUrl; + /** * @param serviceRegistry The serviceRegistry to set. */ @@ -94,7 +95,17 @@ public class ScriptActionExecuter extends ActionExecuterAbstractBase { this.scriptLocation = scriptLocation; } - + + /** + * Set the web application context url + * + * @param webApplicationContextUrl web application context url + */ + public void setWebApplicationContextUrl(String webApplicationContextUrl) + { + this.webApplicationContextUrl = webApplicationContextUrl; + } + /** * Allow adhoc properties to be passed to this action * @@ -149,7 +160,9 @@ public class ScriptActionExecuter extends ActionExecuterAbstractBase // Add the action to the default model ScriptAction scriptAction = new ScriptAction(this.serviceRegistry, action, this.actionDefinition); model.put("action", scriptAction); - + + model.put("webApplicationContextUrl", webApplicationContextUrl); + Object result = null; if (this.scriptLocation == null) { diff --git a/source/java/org/alfresco/repo/admin/patch/impl/ImapFoldersPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/ImapFoldersPatch.java new file mode 100755 index 0000000000..787d4c7a7f --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/impl/ImapFoldersPatch.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.admin.patch.impl; + +import java.io.IOException; +import java.util.List; +import java.util.Properties; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.repo.admin.patch.AbstractPatch; +import org.alfresco.repo.importer.ACPImportPackageHandler; +import org.alfresco.repo.importer.ImporterBootstrap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.admin.PatchException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.springframework.context.MessageSource; +import org.springframework.core.io.ClassPathResource; +/** + * Builds folders tree necessary for IMAP functionality and imports email action scripts. + * + * 1. Company Home > Data Dictionary > Imap Config > Templates + * 2. Company Home > Data Dictionary > Email Actions > search + * 3. Company Home > Data Dictionary > Scripts > command-processor.js, command-search.js + * + * @author Arseny Kovalchuk + */ +public class ImapFoldersPatch extends AbstractPatch +{ + // messages' ids + private static final String MSG_EXISTS = "patch.imapFolders.result.exists"; + private static final String MSG_CREATED = "patch.imapFolders.result.created"; + + // folders' names for path building + private static final String PROPERTY_COMPANY_HOME_CHILDNAME = "spaces.company_home.childname"; + private static final String PROPERTY_DICTIONARY_CHILDNAME = "spaces.dictionary.childname"; + private static final String PROPERTY_SCRIPTS_CHILDNAME = "spaces.scripts.childname"; + private static final String PROPERTY_IMAP_CONFIG_CHILDNAME = "spaces.imapConfig.childname"; + + private ImporterBootstrap importerBootstrap; + private MessageSource messageSource; + protected Properties configuration; + private ImporterService importerService; + + private NodeRef companyHomeNodeRef; + private NodeRef dictionaryNodeRef; + private NodeRef scriptsNodeRef; + private NodeRef imapConfigFolderNodeRef; + private String configFoldersACP; + private String emailActionsACP; + private String scriptsACP; + + public void setImporterBootstrap(ImporterBootstrap importerBootstrap) + { + this.importerBootstrap = importerBootstrap; + } + + public void setMessageSource(MessageSource messageSource) + { + this.messageSource = messageSource; + } + + public void setImporterService(ImporterService importerService) + { + this.importerService = importerService; + } + + public void setConfigFoldersACP(String configFoldersACP) + { + this.configFoldersACP = configFoldersACP; + } + + public void setEmailActionsACP(String emailActionsACP) + { + this.emailActionsACP = emailActionsACP; + } + + public void setScriptsACP(String scriptsACP) + { + this.scriptsACP = scriptsACP; + } + + protected void checkCommonProperties() throws Exception + { + checkPropertyNotNull(importerBootstrap, "importerBootstrap"); + checkPropertyNotNull(messageSource, "messageSource"); + checkPropertyNotNull(importerService, "importerService"); + } + + protected void setUp() throws Exception + { + // get the node store that we must work against + StoreRef storeRef = importerBootstrap.getStoreRef(); + if (storeRef == null) + { + throw new PatchException("Bootstrap store has not been set"); + } + NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); + + this.configuration = importerBootstrap.getConfiguration(); + // get the association names that form the path + String companyHomeChildName = configuration.getProperty(PROPERTY_COMPANY_HOME_CHILDNAME); + if (companyHomeChildName == null || companyHomeChildName.length() == 0) + { + throw new PatchException("Bootstrap property '" + PROPERTY_COMPANY_HOME_CHILDNAME + "' is not present"); + } + String dictionaryChildName = configuration.getProperty(PROPERTY_DICTIONARY_CHILDNAME); + if (dictionaryChildName == null || dictionaryChildName.length() == 0) + { + throw new PatchException("Bootstrap property '" + PROPERTY_DICTIONARY_CHILDNAME + "' is not present"); + } + String scriptsChildName = configuration.getProperty(PROPERTY_SCRIPTS_CHILDNAME); + if (scriptsChildName == null || scriptsChildName.length() == 0) + { + throw new PatchException("Bootstrap property '" + PROPERTY_SCRIPTS_CHILDNAME + "' is not present"); + } + + String imapConfigChildName = configuration.getProperty(PROPERTY_IMAP_CONFIG_CHILDNAME); + if (imapConfigChildName == null || imapConfigChildName.length() == 0) + { + throw new PatchException("Bootstrap property '" + PROPERTY_IMAP_CONFIG_CHILDNAME + "' is not present"); + } + + // build the search string to get the company home node + StringBuilder sb = new StringBuilder(256); + sb.append("/").append(companyHomeChildName); + String xpath = sb.toString(); + // get the company home + List nodeRefs = searchService.selectNodes(storeRootNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() == 0) + { + throw new PatchException("XPath didn't return any results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath); + } + else if (nodeRefs.size() > 1) + { + throw new PatchException("XPath returned too many results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath + "\n" + " results: " + nodeRefs); + } + this.companyHomeNodeRef = nodeRefs.get(0); + + // build the search string to get the dictionary node + sb.append("/").append(dictionaryChildName); + xpath = sb.toString(); + // get the dictionary node + nodeRefs = searchService.selectNodes(storeRootNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() == 0) + { + throw new PatchException("XPath didn't return any results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath); + } + else if (nodeRefs.size() > 1) + { + throw new PatchException("XPath returned too many results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath + "\n" + " results: " + nodeRefs); + } + this.dictionaryNodeRef = nodeRefs.get(0); + sb.append("/").append(scriptsChildName); + xpath = sb.toString(); + nodeRefs = searchService.selectNodes(storeRootNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() == 0) + { + throw new PatchException("XPath didn't return any results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath); + } + else if (nodeRefs.size() > 1) + { + throw new PatchException("XPath returned too many results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath + "\n" + " results: " + nodeRefs); + } + this.scriptsNodeRef = nodeRefs.get(0); + // get the ImapConfig node + sb.delete((sb.length() - (scriptsChildName.length() + 1)), sb.length()); + sb.append("/").append(imapConfigChildName); + xpath = sb.toString(); + nodeRefs = searchService.selectNodes(storeRootNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() > 1) + { + throw new PatchException("XPath returned too many results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath + "\n" + " results: " + nodeRefs); + } + else if (nodeRefs.size() == 0) + { + this.imapConfigFolderNodeRef = null; + } + else + { + this.imapConfigFolderNodeRef = nodeRefs.get(0); + } + + } + + @Override + protected String applyInternal() throws Exception + { + checkCommonProperties(); + setUp(); + String msg = null; + if (imapConfigFolderNodeRef == null) + { + // import the content + RunAsWork importRunAs = new RunAsWork() + { + public Object doWork() throws Exception + { + importImapConfig(); + importScripts(); + importEmailActions(); + return null; + } + }; + AuthenticationUtil.runAs(importRunAs, authenticationContext.getSystemUserName()); + msg = I18NUtil.getMessage(MSG_CREATED); + } + else + { + msg = I18NUtil.getMessage(MSG_EXISTS, imapConfigFolderNodeRef); + } + return msg; + + } + + private void importImapConfig() throws IOException + { + importInternal(this.configFoldersACP, this.dictionaryNodeRef); + } + + private void importEmailActions() throws IOException + { + importInternal(this.emailActionsACP, this.dictionaryNodeRef); + } + + private void importScripts() throws IOException + { + importInternal(this.scriptsACP, this.scriptsNodeRef); + } + + private void importInternal(String acpName, NodeRef space) throws IOException + { + ClassPathResource acpResource = new ClassPathResource(acpName); + ACPImportPackageHandler acpHandler = new ACPImportPackageHandler(acpResource.getFile(), null); + Location importLocation = new Location(space); + importerService.importView(acpHandler, importLocation, null, null); + } + +} diff --git a/source/java/org/alfresco/repo/admin/patch/impl/ImapUsersPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/ImapUsersPatch.java new file mode 100755 index 0000000000..699ffa3e8d --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/impl/ImapUsersPatch.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * This program 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 General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * As a special exception to the terms and conditions of version 2.0 of. + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.admin.patch.impl; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ApplicationModel; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.admin.patch.AbstractPatch; +import org.alfresco.repo.importer.ImporterBootstrap; +import org.alfresco.service.cmr.admin.PatchException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.context.MessageSource; + +/** + * @author Dmitry Vaserin + */ +public class ImapUsersPatch extends AbstractPatch +{ + // messages' ids + private static final String MSG_EXISTS = "patch.imapUserFolders.result.exists"; + private static final String MSG_CREATED = "patch.imapUserFolders.result.created"; + + // folders' names for path building + private static final String PROPERTY_COMPANY_HOME_CHILDNAME = "spaces.company_home.childname"; + private static final String PROPERTY_IMAP_HOME_FOLDER_CHILDNAME = "spaces.imap_home.childname"; + + private static final String PROPERTY_ICON = "space-icon-default"; + private static final String MSG_IMAP_HOME_FOLDER_NAME = "spaces.imap_home.name"; + private static final String MSG_IMAP_HOME_FOLDER_DESCRIPTION = "spaces.imap_home.description"; + + private static final String INBOX_NAME = "INBOX"; + private static final String INBOX_DECSRIPTION = "INBOX mail box"; + + private ImporterBootstrap importerBootstrap; + private MessageSource messageSource; + + protected NodeRef companyHomeNodeRef; + protected Properties configuration; + protected NodeRef imapHomeNodeRef; + + public void setImporterBootstrap(ImporterBootstrap importerBootstrap) + { + this.importerBootstrap = importerBootstrap; + } + + public void setMessageSource(MessageSource messageSource) + { + this.messageSource = messageSource; + } + + /** + * Ensure that required common properties have been set + */ + protected void checkCommonProperties() throws Exception + { + checkPropertyNotNull(importerBootstrap, "importerBootstrap"); + checkPropertyNotNull(namespaceService, "namespaceService"); + checkPropertyNotNull(searchService, "searchService"); + checkPropertyNotNull(nodeService, "nodeService"); + checkPropertyNotNull(messageSource, "messageSource"); + } + + /** + * Extracts pertinent references and properties that are common to execution of this and derived patches. + */ + protected void setUp() throws Exception + { + // get the node store that we must work against + StoreRef storeRef = importerBootstrap.getStoreRef(); + if (storeRef == null) + { + throw new PatchException("Bootstrap store has not been set"); + } + NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); + + this.configuration = importerBootstrap.getConfiguration(); + // get the association names that form the path + String companyHomeChildName = configuration.getProperty(PROPERTY_COMPANY_HOME_CHILDNAME); + if (companyHomeChildName == null || companyHomeChildName.length() == 0) + { + throw new PatchException("Bootstrap property '" + PROPERTY_COMPANY_HOME_CHILDNAME + "' is not present"); + } + String imapHomeChildName = configuration.getProperty(PROPERTY_IMAP_HOME_FOLDER_CHILDNAME); + if (imapHomeChildName == null || imapHomeChildName.length() == 0) + { + throw new PatchException("Bootstrap property '" + PROPERTY_IMAP_HOME_FOLDER_CHILDNAME + "' is not present"); + } + + // build the search string to get the company home node + StringBuilder sb = new StringBuilder(256); + sb.append("/").append(companyHomeChildName); + String xpath = sb.toString(); + // get the company home + List nodeRefs = searchService.selectNodes(storeRootNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() == 0) + { + throw new PatchException("XPath didn't return any results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath); + } + else if (nodeRefs.size() > 1) + { + throw new PatchException("XPath returned too many results: \n" + " root: " + storeRootNodeRef + "\n" + " xpath: " + xpath + "\n" + " results: " + nodeRefs); + } + this.companyHomeNodeRef = nodeRefs.get(0); + + xpath = imapHomeChildName; + nodeRefs = searchService.selectNodes(companyHomeNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() > 1) + { + throw new PatchException("XPath returned too many results: \n" + " dictionary node: " + companyHomeNodeRef + "\n" + " xpath: " + xpath + "\n" + " results: " + + nodeRefs); + } + else if (nodeRefs.size() == 0) + { + // the node does not exist + this.imapHomeNodeRef = null; + } + else + { + this.imapHomeNodeRef = nodeRefs.get(0); + } + } + + @Override + protected String applyInternal() throws Exception + { + // properties must be set + checkCommonProperties(); + // get useful values + setUp(); + + String msg = null; + + if (imapHomeNodeRef == null) + { + // create it + createImapHomeFolders(); + msg = I18NUtil.getMessage(MSG_CREATED, imapHomeNodeRef); + } + else + { + // it already exists + msg = I18NUtil.getMessage(MSG_EXISTS, imapHomeNodeRef); + } + + // done + return msg; + } + + private void createImapHomeFolders() + { + // get required properties + String imapHomeChildName = configuration.getProperty(PROPERTY_IMAP_HOME_FOLDER_CHILDNAME); + if (imapHomeChildName == null) + { + throw new PatchException("Bootstrap property '" + PROPERTY_IMAP_HOME_FOLDER_CHILDNAME + "' is not present"); + } + + String name = messageSource.getMessage(MSG_IMAP_HOME_FOLDER_NAME, null, I18NUtil.getLocale()); + if (name == null || name.length() == 0) + { + throw new PatchException("Bootstrap property '" + MSG_IMAP_HOME_FOLDER_NAME + "' is not present"); + } + + String description = messageSource.getMessage(MSG_IMAP_HOME_FOLDER_DESCRIPTION, null, I18NUtil.getLocale()); + if (description == null || description.length() == 0) + { + throw new PatchException("Bootstrap property '" + MSG_IMAP_HOME_FOLDER_DESCRIPTION + "' is not present"); + } + + imapHomeNodeRef = createSpace(companyHomeNodeRef, name, description, imapHomeChildName); + + // Create IMAP Home and "INBOX" for each user + createImapUserHomes(); + + } + + private void createImapUserHomes() + { + StringBuilder query = new StringBuilder(128); + query.append("@").append(NamespaceService.CONTENT_MODEL_PREFIX).append("\\:userName:*"); + + SearchParameters params = new SearchParameters(); + params.setLanguage(SearchService.LANGUAGE_LUCENE); + params.addStore(importerBootstrap.getStoreRef()); + params.setQuery(query.toString()); + + ResultSet results = searchService.query(params); + List people; + try + { + people = results.getNodeRefs(); + } + finally + { + results.close(); + } + + for (NodeRef nodeRef : people) + { + String userName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME); + String desc = userName + " " + messageSource.getMessage(MSG_IMAP_HOME_FOLDER_NAME, null, I18NUtil.getLocale()); + NodeRef userHome = createSpace(imapHomeNodeRef, userName, desc, userName); + // Create Inbox + createSpace(userHome, INBOX_NAME, INBOX_DECSRIPTION, INBOX_NAME); + + } + + } + + private NodeRef createSpace(NodeRef parent, String name, String desc, String childName) + { + Map properties = new HashMap(7); + properties.put(ContentModel.PROP_NAME, name); + properties.put(ContentModel.PROP_TITLE, name); + properties.put(ContentModel.PROP_DESCRIPTION, desc); + properties.put(ApplicationModel.PROP_ICON, PROPERTY_ICON); + // create the node + ChildAssociationRef childAssocRef = nodeService.createNode(parent, ContentModel.ASSOC_CONTAINS, QName.resolveToQName(namespaceService, childName), + ContentModel.TYPE_FOLDER, properties); + NodeRef result = childAssocRef.getChildRef(); + + // add the required aspects + nodeService.addAspect(result, ApplicationModel.ASPECT_UIFACETS, null); + return result; + } +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapConst.java b/source/java/org/alfresco/repo/imap/AlfrescoImapConst.java new file mode 100755 index 0000000000..6fa28f2150 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapConst.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +/** + * @author Mike Shavnev + */ +public interface AlfrescoImapConst +{ + + public static final char HIERARCHY_DELIMITER = '.'; + public static final String NAMESPACE_PREFIX = "#"; + public static final String USER_NAMESPACE = "#mail"; + public static final String INBOX_NAME = "INBOX"; + + public static final String BODY_TEXT_PLAIN_NAME = "Body.txt"; + public static final String BODY_TEXT_HTML_NAME = "Body.html"; + public static final String MESSAGE_PREFIX = "Message_"; + // Separator for user enties in flag and subscribe properties + public static final String USER_SEPARATOR = ";"; + + /** + * Defines {@link AlfrescoImapMailFolder} view mode as archive mode. Used for Email Archive View. + */ + public static final String MODE_ARCHIVE = "archive"; + /** + * Defines {@link AlfrescoImapMailFolder} view mode as virtual mode. Used for IMAP Virtualised View. + */ + public static final String MODE_VIRTUAL = "virtual"; + + // Default content model email message templates + public static final String CLASSPATH_TEXT_PLAIN_TEMPLATE = "/alfresco/templates/imap/imap_message_text_plain.ftl"; + public static final String CLASSPATH_TEXT_HTML_TEMPLATE = "/alfresco/templates/imap/imap_message_text_html.ftl"; + + public static final String DICTIONARY_TEMPLATE_PREFIX = "emailbody"; + public static final String PREF_IMAP_FAVOURITE_SITES = "org.alfresco.share.sites.imap.favourites"; + + // AlfrescoImapMessage constants + public static final String MIME_VERSION = "MIME-Version"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; + public static final String MULTIPART_MIXED = "mixed"; + public static final String CONTENT_ID = "Content-ID"; + public static final String X_ALF_NODEREF_ID = "X-Alfresco-NodeRef-ID"; // The NodeRef id header + public static final String X_ALF_SERVER_UID = "X-Alfresco-Server-UID"; // The unique identifier of Alfresco server + public static final String EIGHT_BIT_ENCODING = "8bit"; + public static final String BASE_64_ENCODING = "base64"; + public static final String UTF_8 = "UTF-8"; + +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java b/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java new file mode 100755 index 0000000000..34bd71f0c9 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapHostManager.java @@ -0,0 +1,958 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.ImapModel; +import org.alfresco.repo.imap.config.ImapConfigElement.ImapConfig; +import org.alfresco.repo.imap.exception.AlfrescoImapFolderException; +import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.icegreen.greenmail.imap.AuthorizationException; +import com.icegreen.greenmail.imap.ImapHostManager; +import com.icegreen.greenmail.store.FolderException; +import com.icegreen.greenmail.store.MailFolder; +import com.icegreen.greenmail.user.GreenMailUser; +import com.icegreen.greenmail.util.GreenMailUtil; + +/** + * @author Mike Shavnev + */ +public class AlfrescoImapHostManager implements ImapHostManager +{ + + private Log logger = LogFactory.getLog(AlfrescoImapHostManager.class); + + private ServiceRegistry serviceRegistry; + + private NodeService nodeService; + private FileFolderService fileFolderService; + private ImapHelper imapHelper; + + /** + * Returns the hierarchy delimiter for mailboxes on this host. + * + * @return The hierarchy delimiter character. + */ + public char getHierarchyDelimiter() + { + return AlfrescoImapConst.HIERARCHY_DELIMITER; + } + + /** + * Returns an collection of mailboxes. Method searches mailboxes under mount points defined for a specific user. Mount points include user's IMAP Virtualised Views and Email + * Archive Views. This method serves LIST command of the IMAP protocol. + * + * @param user User making the request + * @param mailboxPattern String name of a mailbox possible including a wildcard. + * @return Collection of mailboxes matching the pattern. + * @throws com.icegreen.greenmail.store.FolderException + */ + public Collection listMailboxes(GreenMailUser user, String mailboxPattern) throws FolderException + { + mailboxPattern = GreenMailUtil.convertFromUtf7(mailboxPattern); + if (logger.isDebugEnabled()) + { + logger.debug("Listing mailboxes: mailboxPattern=" + mailboxPattern); + } + mailboxPattern = imapHelper.getMailPathInRepo(mailboxPattern); + if (logger.isDebugEnabled()) + { + logger.debug("Listing mailboxes: mailboxPattern in alfresco=" + mailboxPattern); + } + + Collection result = new LinkedList(); + + Map mountPoints = imapHelper.getMountPoints(); + Map imapConfigs = imapHelper.getImapConfigs(); + + NodeRef mountPoint; + for (String mountPointName : mountPoints.keySet()) + { + + mountPoint = mountPoints.get(mountPointName); + FileInfo mountPointFileInfo = imapHelper.getFileFolderService().getFileInfo(mountPoint); + + NodeRef mountParent = imapHelper.getNodeService().getParentAssocs(mountPoint).get(0).getParentRef(); + String mode = imapConfigs.get(mountPointName).getMode(); + + if (!mailboxPattern.equals("*")) + { + mountPoint = mountParent; + } + + boolean isVirtualView = imapConfigs.get(mountPointName).getMode().equals(AlfrescoImapConst.MODE_VIRTUAL); + Collection folders = listFolder(mountPoint, user, mailboxPattern, isVirtualView); + if (folders != null) + { + for (FileInfo folder : folders) + { + result.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), folder, folder.getName(), mode, mountParent, mountPointName, imapHelper)); + } + } + if (mailboxPattern.equals("*")) + { + result.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), mountPointFileInfo, mountPointName, mode, mountParent, mountPointName, imapHelper)); + } + } + mountPoint = imapHelper.getUserImapHomeRef(user.getLogin()); + Collection imapFolders = listFolder(mountPoint, user, mailboxPattern, false); + if (imapFolders != null) + { + for (FileInfo imapFolder : imapFolders) + { + result.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), imapFolder, imapFolder.getName(), AlfrescoImapConst.MODE_ARCHIVE, mountPoint, null, + imapHelper)); + } + } + + return result; + } + + private Collection listFolder(NodeRef root, GreenMailUser user, String mailboxPattern, boolean isVirtualView) throws FolderException + { + if (logger.isDebugEnabled()) + { + logger.debug("Listing mailboxes: mailboxPattern=" + mailboxPattern); + } + + Collection result = new LinkedList(); + + int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + + String name = null; + String remainName = null; + + if (index < 0) + { + name = mailboxPattern; + } + else + { + name = mailboxPattern.substring(0, index); + remainName = mailboxPattern.substring(index + 1); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Listing mailboxes: name=" + name); + } + + if (index < 0) + { + if ("*".equals(name)) + { + List list = imapHelper.searchFolders(root, name, true, isVirtualView); + if (list.size() > 0) + { + return list; + } + return null; + } + else if (name.endsWith("*")) + { + List list = imapHelper.searchFolders(root, name.replace('%', '*'), false, isVirtualView); + if (list.size() > 0) + { + result.addAll(list); + for (FileInfo fileInfo : list) + { + List childList = imapHelper.searchFolders(fileInfo.getNodeRef(), "*", true, isVirtualView); + result.addAll(childList); + } + return result; + } + return null; + } + else if (name.contains("%") || name.contains("*")) + { + List list = imapHelper.searchFolders(root, name.replace('%', '*'), false, isVirtualView); + if (list.size() > 0) + { + return list; + } + return null; + } + else + { + List list = imapHelper.searchFolders(root, name, false, isVirtualView); + if (list.size() > 0) + { + return list; + } + return null; + } + } + + List list = imapHelper.searchFolders(root, name.replace('%', '*'), false, isVirtualView); + for (FileInfo folder : list) + { + Collection childFolders = listFolder(folder.getNodeRef(), user, remainName, isVirtualView); + if (childFolders != null) + { + result.addAll(childFolders); + } + } + + if (result.isEmpty()) + { + return null; + } + + return result; + } + + /** + * Returns an collection of subscribed mailboxes. To appear in search result mailboxes should have {http://www.alfresco.org/model/imap/1.0}subscribed property specified for + * user. Method searches subscribed mailboxes under mount points defined for a specific user. Mount points include user's IMAP Virtualised Views and Email Archive Views. This + * method serves LSUB command of the IMAP protocol. + * + * @param user User making the request + * @param mailboxPattern String name of a mailbox possible including a wildcard. + * @return Collection of mailboxes matching the pattern. + * @throws com.icegreen.greenmail.store.FolderException + */ + public Collection listSubscribedMailboxes(GreenMailUser user, String mailboxPattern) throws FolderException + { + if (logger.isDebugEnabled()) + { + logger.debug("Listing subscribed mailboxes: mailboxPattern=" + mailboxPattern); + } + mailboxPattern = imapHelper.getMailPathInRepo(mailboxPattern); + if (logger.isDebugEnabled()) + { + logger.debug("Listing subscribed mailboxes: mailboxPattern in alfresco=" + mailboxPattern); + } + + Collection result = new LinkedList(); + + Map mountPoints = imapHelper.getMountPoints(); + Map imapConfigs = imapHelper.getImapConfigs(); + + NodeRef mountPoint; + + for (String mountPointName : mountPoints.keySet()) + { + + mountPoint = mountPoints.get(mountPointName); + FileInfo mountPointFileInfo = imapHelper.getFileFolderService().getFileInfo(mountPoint); + NodeRef mountParent = imapHelper.getNodeService().getParentAssocs(mountPoint).get(0).getParentRef(); + String viewMode = imapConfigs.get(mountPointName).getMode(); + + if (!mailboxPattern.equals("*")) + { + mountPoint = mountParent; + } + + boolean isVirtualView = imapConfigs.get(mountPointName).getMode().equals(AlfrescoImapConst.MODE_VIRTUAL); + Collection folders = listSubscribedFolder(mountPoint, mountPoint, user, mailboxPattern, isVirtualView); + if (folders != null) + { + for (MailFolder mailFolder : folders) + { + AlfrescoImapMailFolder folder = (AlfrescoImapMailFolder) mailFolder; + folder.setMountPointName(mountPointName); + folder.setViewMode(viewMode); + folder.setMountParent(mountParent); + } + result.addAll(folders); + } + + if (mailboxPattern.equals("*")) + { + if (isSubscribed(mountPointFileInfo, user.getLogin())) + { + result.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), mountPointFileInfo, mountPointName, viewMode, mountParent, mountPointName, imapHelper)); + } + // \NoSelect + else if (hasSubscribedChild(mountPointFileInfo, user.getLogin(), isVirtualView)) + { + result.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), mountPointFileInfo, mountPointName, viewMode, mountParent, mountPointName, imapHelper, + false)); + } + } + + } + + NodeRef root = imapHelper.getSpacesStoreNodeRef(); + + root = imapHelper.getUserImapHomeRef(user.getLogin()); + Collection imapFolders = listSubscribedFolder(root, root, user, mailboxPattern, false); + + if (imapFolders != null) + { + result.addAll(imapFolders); + } + + return result; + } + + private Collection listSubscribedFolder(NodeRef mailboxRoot, NodeRef root, GreenMailUser user, String mailboxPattern, boolean isVirtualView) throws FolderException + { + if (logger.isDebugEnabled()) + { + logger.debug("Listing mailboxes: mailboxPattern=" + mailboxPattern); + } + + int index = mailboxPattern.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + + String name = null; + String remainName = null; + + if (index < 0) + { + name = mailboxPattern; + } + else + { + name = mailboxPattern.substring(0, index); + remainName = mailboxPattern.substring(index + 1); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Listing mailboxes: name=" + name); + } + + if (index < 0) + { + if ("*".equals(name)) + { + List list = imapHelper.searchFolders(root, name, true, isVirtualView); + Collection subscribedList = getSubscribed(list, user.getLogin()); + + if (subscribedList.size() > 0) + { + return createMailFolderList(user, subscribedList, mailboxRoot); + } + return null; + } + else if (name.endsWith("*")) + { + List fullList = new LinkedList(); + List list = imapHelper.searchFolders(root, name.replace('%', '*'), false, isVirtualView); + Collection subscribedList = getSubscribed(list, user.getLogin()); + + if (list.size() > 0) + { + fullList.addAll(subscribedList); + for (FileInfo fileInfo : list) + { + List childList = imapHelper.searchFolders(fileInfo.getNodeRef(), "*", true, isVirtualView); + fullList.addAll(getSubscribed(childList, user.getLogin())); + } + return createMailFolderList(user, fullList, mailboxRoot); + } + return null; + } + else if ("%".equals(name)) + { + List list = imapHelper.searchFolders(root, "*", false, isVirtualView); + LinkedList subscribedList = new LinkedList(); + + for (FileInfo fileInfo : list) + { + if (isSubscribed(fileInfo, user.getLogin())) + { + // folderName, viewMode, mountPointName will be setted in listSubscribedMailboxes() method + subscribedList.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), fileInfo, null, null, mailboxRoot, null, imapHelper)); + } + // \NoSelect + else if (hasSubscribedChild(fileInfo, user.getLogin(), isVirtualView)) + { + // folderName, viewMode, mountPointName will be setted in listSubscribedMailboxes() method + subscribedList.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), fileInfo, null, null, mailboxRoot, null, imapHelper, false)); + } + } + + return subscribedList; + } + else if (name.contains("%") || name.contains("*")) + { + List list = imapHelper.searchFolders(root, name.replace('%', '*'), false, isVirtualView); + Collection subscribedList = getSubscribed(list, user.getLogin()); + + if (subscribedList.size() > 0) + { + return createMailFolderList(user, subscribedList, mailboxRoot); + } + return null; + } + else + { + List list = imapHelper.searchFolders(root, name, false, isVirtualView); + Collection subscribedList = getSubscribed(list, user.getLogin()); + + if (subscribedList.size() > 0) + { + return createMailFolderList(user, subscribedList, mailboxRoot); + } + return null; + } + } + + // If (index != -1) this is not the last level + Collection result = new LinkedList(); + + List list = imapHelper.searchFolders(root, name.replace('%', '*'), false, isVirtualView); + for (FileInfo folder : list) + { + Collection childFolders = listSubscribedFolder(mailboxRoot, folder.getNodeRef(), user, remainName, isVirtualView); + + if (childFolders != null) + { + result.addAll(childFolders); + } + } + + if (result.isEmpty()) + { + return null; + } + + return result; + } + + /** + * Renames an existing mailbox. The specified mailbox must already exist, the requested name must not exist already but must be able to be created and the user must have rights + * to delete the existing mailbox and create a mailbox with the new name. Any inferior hierarchical names must also be renamed. If INBOX is renamed, the contents of INBOX are + * transferred to a new mailbox with the new name, but INBOX is not deleted. If INBOX has inferior mailbox these are not renamed. This method serves RENAME command of the IMAP + * protocol.

Method searches mailbox under mount points defined for a specific user. Mount points include user's IMAP Virtualised Views and Email Archive Views. + * + * @param user User making the request. + * @param oldMailboxName String name of the existing folder + * @param newMailboxName String target new name + * @throws com.icegreen.greenmail.store.FolderException if an existing folder with the new name. + * @throws AlfrescoImapFolderException if user does not have rights to create the new mailbox. + */ + + public void renameMailbox(GreenMailUser user, String oldMailboxName, String newMailboxName) throws FolderException, AuthorizationException + { + oldMailboxName = GreenMailUtil.convertFromUtf7(oldMailboxName); + newMailboxName = GreenMailUtil.convertFromUtf7(newMailboxName); + if (logger.isDebugEnabled()) + { + logger.debug("Renaming folder: oldMailboxName=" + oldMailboxName + " newMailboxName=" + newMailboxName); + } + + AlfrescoImapMailFolder sourceNode = (AlfrescoImapMailFolder) getFolder(user, GreenMailUtil.convertInUtf7(oldMailboxName)); + + NodeRef root = imapHelper.getMailboxRootRef(oldMailboxName, user.getLogin()); + String mailboxRepoName = imapHelper.getMailPathInRepo(newMailboxName); + + StringTokenizer tokenizer = new StringTokenizer(mailboxRepoName, String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); + + NodeRef parentNodeRef = root; + while (tokenizer.hasMoreTokens()) + { + String folderName = tokenizer.nextToken(); + + if (!tokenizer.hasMoreTokens()) + { + 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(), parentNodeRef, folderName); + List itemsForRemove = fileFolderService.list(sourceNode.getFolderInfo().getNodeRef()); + for (FileInfo fileInfo : itemsForRemove) + { + fileFolderService.delete(fileInfo.getNodeRef()); + } + + } + else + { + fileFolderService.move(sourceNode.getFolderInfo().getNodeRef(), parentNodeRef, folderName); + } + return; + } + catch (FileExistsException e) + { + throw new FolderException(FolderException.ALREADY_EXISTS_LOCALLY); + } + catch (FileNotFoundException e) + { + if (logger.isDebugEnabled()) + { + logger.error(e); + } + } + + } + else + { + List folders = imapHelper.searchFolders(parentNodeRef, folderName, false, true); + + if (folders.size() == 0) + { + AccessStatus status = imapHelper.hasPermission(parentNodeRef, PermissionService.WRITE); + if (status == AccessStatus.DENIED) + { + if (logger.isDebugEnabled()) + { + logger.debug("Creating folder: Cant't create folder - Permission denied"); + } + throw new AlfrescoImapFolderException(AlfrescoImapFolderException.PERMISSION_DENIED); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Create mailBox: " + folderName); + } + FileFolderServiceImpl.makeFolders(fileFolderService, parentNodeRef, Arrays.asList(folderName), ContentModel.TYPE_FOLDER); + } + else + { + parentNodeRef = folders.get(0).getNodeRef(); + if (logger.isDebugEnabled()) + { + logger.debug("MailBox: " + folderName + " already exists"); + } + } + } + } + + } + + /** + * Returns a reference to a newly created mailbox. The request should specify a mailbox that does not already exist on this server, that could exist on this server and that the + * user has rights to create. This method serves CREATE command of the IMAP protocol. + * + * @param user User making the request. + * @param mailboxName String name of the target + * @return an Mailbox reference. + * @throws com.icegreen.greenmail.store.FolderException if mailbox already exists + * @throws AlfrescoImapFolderException if user does not have rights to create the new mailbox. + */ + public MailFolder createMailbox(GreenMailUser user, String mailboxName) throws AuthorizationException, FolderException + { + mailboxName = GreenMailUtil.convertFromUtf7(mailboxName); + if (logger.isDebugEnabled()) + { + logger.debug("Creating folder: " + mailboxName); + } + + NodeRef root = imapHelper.getMailboxRootRef(mailboxName, user.getLogin()); + + String mountPointName = imapHelper.getMountPointName(mailboxName); + String mailboxRepoNam = imapHelper.getMailPathInRepo(mailboxName); + StringTokenizer tokenizer = new StringTokenizer(mailboxRepoNam, String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); + + NodeRef parentNodeRef = root; + + while (tokenizer.hasMoreTokens()) + { + String folderName = tokenizer.nextToken(); + + List folders = imapHelper.searchFolders(parentNodeRef, folderName, false, true); + + if (folders.size() == 0) + { + AccessStatus status = imapHelper.hasPermission(parentNodeRef, PermissionService.WRITE); + if (status == AccessStatus.DENIED) + { + if (logger.isDebugEnabled()) + { + logger.debug("Creating folder: Cant't create folder - Permission denied"); + } + throw new AlfrescoImapFolderException(AlfrescoImapFolderException.PERMISSION_DENIED); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Create mailBox: " + mailboxName); + } + FileInfo mailFolder = FileFolderServiceImpl.makeFolders(fileFolderService, parentNodeRef, Arrays.asList(folderName), ContentModel.TYPE_FOLDER); + + + return new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), mailFolder, folderName, imapHelper.getViewMode(mailboxName), root, mountPointName, imapHelper); + + } + else + { + parentNodeRef = folders.get(0).getNodeRef(); + if (logger.isDebugEnabled()) + { + logger.debug("MailBox: " + folderName + " already exists"); + } + } + } + + throw new FolderException(FolderException.ALREADY_EXISTS_LOCALLY); + } + + /** + * Deletes an existing MailBox. Specified mailbox must already exist on this server, and the user must have rights to delete it.

This method serves DELETE command of the + * IMAP protocol. + * + * @param user User making the request. + * @param mailboxName String name of the target + * @throws com.icegreen.greenmail.store.FolderException if mailbox has a non-selectable store with children + */ + public void deleteMailbox(GreenMailUser user, String mailboxName) throws FolderException, AuthorizationException + { + AlfrescoImapMailFolder folder = (AlfrescoImapMailFolder) getFolder(user, mailboxName); + NodeRef nodeRef = folder.getFolderInfo().getNodeRef(); + + List childFolders = imapHelper.searchFolders(nodeRef, "*", false, false); + + if (childFolders.isEmpty()) + { + folder.signalDeletion(); + // Delete child folders and messages + fileFolderService.delete(nodeRef); + } + else + { + if (folder.isSelectable()) + { + // Delete all messages for this folder + // Don't delete subfolders and their messages + List messages = imapHelper.searchFiles(nodeRef, "*", ImapModel.TYPE_IMAP_CONTENT, false); + for (FileInfo message : messages) + { + fileFolderService.delete(message.getNodeRef()); + } + nodeService.addAspect(nodeRef, ImapModel.ASPECT_IMAP_FOLDER_NONSELECTABLE, null); + } + else + { + throw new FolderException(mailboxName + " - Can't delete a non-selectable store with children."); + } + } + } + + /** + * Returns a reference to an existing Mailbox. The requested mailbox must already exists on this server and the requesting user must have at least lookup rights.

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

C: 2 list "" ""

S: * LIST () "." ""

S: 2 OK LIST completed.

Method searches + * mailbox under mount points defined for a specific user. Mount points include user's IMAP Virtualised Views and Email Archive Views. + * + * @param user User making the request. + * @param mailboxName String name of the target. + * @return an Mailbox reference. + */ + public MailFolder getFolder(GreenMailUser user, String mailboxName) + { + mailboxName = GreenMailUtil.convertFromUtf7(mailboxName); + if (logger.isDebugEnabled()) + { + logger.debug("Getting folder: " + mailboxName); + } + + // If MailFolder object is used to obtain hierarchy delimiter by LIST command: + // Example: + // C: 2 list "" "" + // S: * LIST () "." "" + // S: 2 OK LIST completed. + if ("".equals(mailboxName)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Request for the hierarchy delimiter"); + } + return new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), null, null, null, null, null, null); + } + + NodeRef root = imapHelper.getMailboxRootRef(mailboxName, user.getLogin()); + String mountPointName = imapHelper.getMountPointName(mailboxName); + String mailboxRepoName = imapHelper.getMailPathInRepo(mailboxName); + + StringTokenizer tokenizer = new StringTokenizer(mailboxRepoName, String.valueOf(AlfrescoImapConst.HIERARCHY_DELIMITER)); + int count = tokenizer.countTokens(); + NodeRef nodeRef = root; + + while (tokenizer.hasMoreTokens()) + { + String t = tokenizer.nextToken(); + if (logger.isDebugEnabled()) + { + logger.debug("token=" + t); + } + count--; + + List list = imapHelper.searchFolders(nodeRef, t, false, true); + + if (count == 0) + { + if (!list.isEmpty()) + { + + return new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), list.get(0), list.get(0).getName(), imapHelper.getViewMode(mailboxName), root, + mountPointName, imapHelper); + } + else + { + return new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), null, null, null, null, null, null); + } + } + else + { + if (!list.isEmpty()) + { + nodeRef = list.get(0).getNodeRef(); + } + else + { + return new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), null, null, null, null, null, null); + } + } + } + + throw new IllegalStateException("Error state"); + } + + /** + * Simply calls {@link #getFolder(GreenMailUser, String)}.

Added to implement {@link ImapHostManager}. + */ + public MailFolder getFolder(GreenMailUser user, String mailboxName, boolean mustExist) throws FolderException + { + return getFolder(user, mailboxName); + } + + /** + * Returns a reference to the user's INBOX. + * + * @param user The user making the request. + * @return The user's Inbox. + */ + public MailFolder getInbox(GreenMailUser user) throws FolderException + { + return getFolder(user, AlfrescoImapConst.INBOX_NAME); + } + + /** + * Not supported. May be used by GreenMailUser.create() method.

Added to implement {@link ImapHostManager}. + */ + public void createPrivateMailAccount(GreenMailUser user) throws FolderException + { + throw new UnsupportedOperationException(); + } + + /** + * Subscribes a user to a mailbox. The mailbox must exist locally and the user must have rights to modify it.

This method serves SUBSCRIBE command of the IMAP protocol. + * + * @param user User making the request + * @param mailbox String representation of a mailbox name. + */ + public void subscribe(final GreenMailUser user, final String mailbox) throws FolderException + { + if (logger.isDebugEnabled()) + { + logger.debug("Subscribing: " + mailbox); + } + AlfrescoImapMailFolder mailFolder = (AlfrescoImapMailFolder) getFolder(user, mailbox); + nodeService.addAspect(mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_SUBSCRIBED, null); +// This is a multiuser support. Commented due new requirements +// AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() +// { +// public Void doWork() throws Exception +// { +// AlfrescoImapMailFolder mailFolder = (AlfrescoImapMailFolder) getFolder(user, mailbox); +// FileInfo fileInfo = mailFolder.getFolderInfo(); +// if (fileInfo != null) +// { +// String subscribedList = (String) nodeService.getProperty(fileInfo.getNodeRef(), ImapModel.PROP_IMAP_FOLDER_SUBSCRIBED); +// if (subscribedList == null) +// { +// subscribedList = ""; +// } +// subscribedList = subscribedList.replaceAll(imapHelper.formatUserEntry(user.getLogin()), ""); +// subscribedList += imapHelper.formatUserEntry(user.getLogin()); +// nodeService.setProperty(fileInfo.getNodeRef(), ImapModel.PROP_IMAP_FOLDER_SUBSCRIBED, subscribedList); +// } +// else +// { +// logger.debug("MailBox: " + mailbox + "doesn't exsist. Maybe it was deleted earlier."); +// } +// return null; +// } +// }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Unsubscribes from a given mailbox.

This method serves UNSUBSCRIBE command of the IMAP protocol. + * + * @param user User making the request + * @param mailbox String representation of a mailbox name. + */ + public void unsubscribe(final GreenMailUser user, final String mailbox) throws FolderException + { + if (logger.isDebugEnabled()) + { + logger.debug("Unsubscribing: " + mailbox); + } + AlfrescoImapMailFolder mailFolder = (AlfrescoImapMailFolder) getFolder(user, mailbox); + nodeService.removeAspect(mailFolder.getFolderInfo().getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_SUBSCRIBED); + +// This is a multiuser support. Commented due new requirements +// AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() +// { +// public Void doWork() throws Exception +// { +// AlfrescoImapMailFolder mailFolder = (AlfrescoImapMailFolder) getFolder(user, mailbox); +// if (mailFolder.getFolderInfo() != null) +// { +// FileInfo fileInfo = mailFolder.getFolderInfo(); +// String subscribedList = (String) nodeService.getProperty(fileInfo.getNodeRef(), ImapModel.PROP_IMAP_FOLDER_SUBSCRIBED); +// if (subscribedList == null) +// { +// subscribedList = ""; +// } +// subscribedList = subscribedList.replaceAll(imapHelper.formatUserEntry(user.getLogin()), ""); +// nodeService.setProperty(fileInfo.getNodeRef(), ImapModel.PROP_IMAP_FOLDER_SUBSCRIBED, subscribedList); +// } +// else +// { +// logger.debug("MailBox: " + mailbox + " doesn't exsist. Maybe it was deleted earlier."); +// } +// +// return null; +// } +// }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Not supported. Used by GreenMail class. + */ + public List getAllMessages() + { + throw new UnsupportedOperationException(); + } + + private boolean isSubscribed(FileInfo fileInfo, String userName) + { + return nodeService.hasAspect(fileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_SUBSCRIBED); +// This is a multiuser support. Commented due new requirements + +// Map properties = fileInfo.getProperties(); +// String subscribedList = (String) properties.get(ImapModel.PROP_IMAP_FOLDER_SUBSCRIBED); +// if (subscribedList == null) +// { +// return false; +// } +// else +// { +// return subscribedList.contains(imapHelper.formatUserEntry(userName)); +// } + + } + + private Collection getSubscribed(List list, String userName) + { + Collection result = new LinkedList(); + + for (FileInfo folderInfo : list) + { + if (isSubscribed(folderInfo, userName)) + { + result.add(folderInfo); + } + } + + return result; + } + + private boolean hasSubscribedChild(FileInfo parent, String userName, boolean isVirtualView) + { + List list = imapHelper.searchFolders(parent.getNodeRef(), "*", true, isVirtualView); + + for (FileInfo fileInfo : list) + { + if (isSubscribed(fileInfo, userName)) + { + return true; + } + } + + return false; + } + + private Collection createMailFolderList(GreenMailUser user, Collection list, NodeRef imapUserHomeRef) + { + Collection result = new LinkedList(); + + for (FileInfo folderInfo : list) + { + // folderName, viewMode, mountPointName will be setted in listSubscribedMailboxes() method + result.add(new AlfrescoImapMailFolder(user.getQualifiedMailboxName(), folderInfo, null, null, imapUserHomeRef, null, imapHelper)); + } + + return result; + + } + + // ----------------------Getters and Setters---------------------------- + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public ServiceRegistry getServiceRegistry() + { + return serviceRegistry; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + public void setImapHelper(ImapHelper imapHelper) + { + this.imapHelper = imapHelper; + } + +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapMailFolder.java b/source/java/org/alfresco/repo/imap/AlfrescoImapMailFolder.java new file mode 100755 index 0000000000..3537733ec6 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapMailFolder.java @@ -0,0 +1,1084 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javax.mail.BodyPart; +import javax.mail.Flags; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Part; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimeUtility; +import javax.mail.search.SearchTerm; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.model.ImapModel; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.icegreen.greenmail.foedus.util.MsgRangeFilter; +import com.icegreen.greenmail.imap.ImapConstants; +import com.icegreen.greenmail.mail.MovingMessage; +import com.icegreen.greenmail.store.FolderException; +import com.icegreen.greenmail.store.FolderListener; +import com.icegreen.greenmail.store.MailFolder; +import com.icegreen.greenmail.store.MessageFlags; +import com.icegreen.greenmail.store.SimpleStoredMessage; +import com.icegreen.greenmail.util.GreenMailUtil; + +/** + * Implementation of greenmail MailFolder. It represents an Alfresco content folder and handles appendMessage, copyMessage, expunge (delete), getMessages, getMessage and so + * requests. + * + * @author Mike Shavnev + */ +public class AlfrescoImapMailFolder implements MailFolder +{ + + private static Log logger = LogFactory.getLog(AlfrescoImapMailFolder.class); + + /** + * Reference to the {@link FileInfo} object representing the folder. + */ + private FileInfo folderInfo; + + /** + * Reference to the root node of the store where folder is placed. + */ + private NodeRef rootNodeRef; + + /** + * Name of the mailbox (e.g. "admin" for admin user). + */ + private String qualifiedMailboxName; + + /** + * Name of the folder. + */ + private String folderName; + + /** + * Defines view mode. Can be one of the following: {@link AlfrescoImapConst#MODE_ARCHIVE} or {@link AlfrescoImapConst#MODE_VIRTUAL}. + */ + private String viewMode; + + /** + * Name of the mount point. + */ + private String mountPointName; + + /** + * Reference to the {@link ImapHelper} object. + */ + private ImapHelper imapHelper; + + /** + * Defines whether the folder is selectable or not. + */ + private Boolean selectable; + + /** + * Defines whether the folder is read-only for user or not. + */ + private Boolean readOnly; + + + private Map messages = new TreeMap(); + private boolean isBodyGenerated = false; + + private static final Flags PERMANENT_FLAGS = new Flags(); + + private List listeners = new LinkedList(); + + static + { + PERMANENT_FLAGS.add(Flags.Flag.ANSWERED); + PERMANENT_FLAGS.add(Flags.Flag.DELETED); + PERMANENT_FLAGS.add(Flags.Flag.DRAFT); + PERMANENT_FLAGS.add(Flags.Flag.FLAGGED); + PERMANENT_FLAGS.add(Flags.Flag.SEEN); + } + + /** + * Constructs {@link AlfrescoImapMailFolder} object. + * + * @param qualifiedMailboxName - name of the mailbox (e.g. "admin" for admin user). + * @param folderInfo - reference to the {@link FileInfo} object representing the folder. + * @param folderName - name of the folder. + * @param viewMode - defines view mode. Can be one of the following: {@link AlfrescoImapConst#MODE_ARCHIVE} or {@link AlfrescoImapConst#MODE_VIRTUAL}. + * @param rootNodeRef - reference to the root node of the store where folder is placed. + * @param mountPointName - name of the mount point. + * @param imapHelper - reference to the {@link ImapHelper} object. + */ + public AlfrescoImapMailFolder(String qualifiedMailboxName, FileInfo folderInfo, String folderName, String viewMode, NodeRef rootNodeRef, String mountPointName, + ImapHelper imapHelper) + { + this(qualifiedMailboxName, folderInfo, folderName, viewMode, rootNodeRef, mountPointName, imapHelper, null); + } + + /** + * Constructs {@link AlfrescoImapMailFolder} object. + * + * @param qualifiedMailboxName - name of the mailbox (e.g. "admin" for admin user). + * @param folderInfo - reference to the {@link FileInfo} object representing the folder. + * @param folderName - name of the folder. + * @param viewMode - defines view mode. Can be one of the following: {@link AlfrescoImapConst#MODE_ARCHIVE} or {@link AlfrescoImapConst#MODE_VIRTUAL}. + * @param rootNodeRef - reference to the root node of the store where folder is placed. + * @param mountPointName - name of the mount point. + * @param imapHelper - reference to the {@link ImapHelper} object. + * @param selectable - defines whether the folder is selectable or not. + */ + public AlfrescoImapMailFolder(String qualifiedMailboxName, FileInfo folderInfo, String folderName, String viewMode, NodeRef rootNodeRef, String mountPointName, + ImapHelper imapHelper, Boolean selectable) + { + this.qualifiedMailboxName = qualifiedMailboxName; + this.folderInfo = folderInfo; + this.rootNodeRef = rootNodeRef; + this.imapHelper = imapHelper; + this.folderName = folderName != null ? folderName : (folderInfo != null ? folderInfo.getName() : null); + this.viewMode = viewMode != null ? viewMode : AlfrescoImapConst.MODE_ARCHIVE; + this.mountPointName = mountPointName; + + // MailFolder object can be null if it is used to obtain hierarchy delimiter by LIST command: + // Example: + // C: 2 list "" "" + // S: * LIST () "." "" + // S: 2 OK LIST completed. + if (folderInfo != null) + { + if (selectable == null) + { + // isSelectable(); + Boolean storedSelectable = !imapHelper.getNodeService().hasAspect(folderInfo.getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSELECTABLE); + if (storedSelectable == null) + { + setSelectable(true); + } + else + { + setSelectable(storedSelectable); + } + } + else + { + setSelectable(selectable); + } + + AccessStatus status = imapHelper.hasPermission(folderInfo.getNodeRef(), PermissionService.WRITE); + if (status == AccessStatus.DENIED) + { + readOnly = true; + } + else + { + readOnly = false; + } + + } + else + { + setSelectable(false); + } + + } + + /** + * Adds {@link FolderListener} to the folder. + * + * @param listener - new listener. + */ + public void addListener(FolderListener listener) + { + listeners.add(listener); + + } + + protected void processTextMessage(MimeMessage message, FileInfo messageHome) throws MessagingException, ContentIOException, IOException + { + FileInfo messageBody = imapHelper.getFileFolderService().create(messageHome.getNodeRef(), AlfrescoImapConst.BODY_TEXT_PLAIN_NAME, ImapModel.TYPE_IMAP_BODY); + ContentWriter writer = imapHelper.getFileFolderService().getWriter(messageBody.getNodeRef()); + writer.setMimetype(message.getContentType()); + writer.setEncoding("UTF-8"); + writer.putContent(message.getInputStream()); + } + + protected void processTextMessage(BodyPart part, FileInfo messageHome, boolean isBody) throws MessagingException, ContentIOException, IOException + { + FileInfo messageBody = null; + ContentType ct = new ContentType(part.getContentType()); + ContentWriter writer = null; + if (isBody) + { + if ("plain".equalsIgnoreCase(ct.getSubType())) + { + messageBody = imapHelper.getFileFolderService().create(messageHome.getNodeRef(), AlfrescoImapConst.BODY_TEXT_PLAIN_NAME, ImapModel.TYPE_IMAP_BODY); + writer = imapHelper.getFileFolderService().getWriter(messageBody.getNodeRef()); + writer.setEncoding(MimeUtility.javaCharset(ct.getParameter("charset"))); + writer.setMimetype(ct.toString()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + IOUtils.copy(part.getInputStream(), outputStream); + OutputStreamWriter outputWriter = new OutputStreamWriter(writer.getContentOutputStream()); + outputWriter.write(outputStream.toString()); + outputWriter.flush(); + outputWriter.close(); + } + else if ("html".equalsIgnoreCase(ct.getSubType())) + { + messageBody = imapHelper.getFileFolderService().create(messageHome.getNodeRef(), AlfrescoImapConst.BODY_TEXT_HTML_NAME, ImapModel.TYPE_IMAP_BODY); + writer = imapHelper.getFileFolderService().getWriter(messageBody.getNodeRef()); + writer.setMimetype(ct.toString()); + String javaCharset = MimeUtility.javaCharset(ct.getParameter("charset")); + writer.setEncoding(javaCharset); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + IOUtils.copy(part.getInputStream(), os); + OutputStreamWriter cosw = new OutputStreamWriter(writer.getContentOutputStream()); + cosw.write(os.toString()); + cosw.flush(); + cosw.close(); + } + } + else + { + saveAttachment(part, messageHome); + } + } + + protected void saveAttachment(BodyPart part, FileInfo messageHome) throws FileExistsException, MessagingException, ContentIOException, IOException + { + FileInfo messageBody = imapHelper.getFileFolderService().create(messageHome.getNodeRef(), MimeUtility.decodeText(part.getFileName()), ImapModel.TYPE_IMAP_ATTACH); + ContentWriter writer = imapHelper.getFileFolderService().getWriter(messageBody.getNodeRef()); + writer.setMimetype(part.getContentType()); + writer.setEncoding("UTF-8"); + writer.putContent(part.getInputStream()); + + String[] attachId = part.getHeader("Content-ID"); + if (attachId != null && attachId.length > 0) + { + imapHelper.getNodeService().setProperty(messageBody.getNodeRef(), ImapModel.PROP_ATTACH_ID, attachId[0]); + } + } + + /** + * Appends message to the folder. + * + * @param message - message. + * @param flags - message flags. + * @param internalDate - not used. Current date used instead. + * @return + */ + public long appendMessage(MimeMessage message, Flags flags, Date internalDate) throws FolderException + { + if (this.readOnly) + { + throw new FolderException("Can't append message - Permission denied"); + } + + //TODO FILE EXIST + String name = AlfrescoImapConst.MESSAGE_PREFIX + GUID.generate(); + FileInfo messageHome = imapHelper.getFileFolderService().create(folderInfo.getNodeRef(), name, ImapModel.TYPE_IMAP_CONTENT); + final long newMessageUid = (Long) messageHome.getProperties().get(ContentModel.PROP_NODE_DBID); + + try + { + name = AlfrescoImapConst.MESSAGE_PREFIX + newMessageUid; + imapHelper.getFileFolderService().rename(messageHome.getNodeRef(), name); + + Object content = message.getContent(); + if (content instanceof Multipart) + { + Multipart multipart = (Multipart) content; + + for (int i = 0, n = multipart.getCount(); i < n; i++) + { + Part part = multipart.getBodyPart(i); + createMessageFiles(messageHome, (MimeBodyPart) part); + + } + } + else + { + processTextMessage(message, messageHome); + } + + imapHelper.setFlags(messageHome, flags, true); + SimpleStoredMessage storedMessage = new SimpleStoredMessage(new AlfrescoImapMessage(messageHome, imapHelper, message), new Date(), newMessageUid); + messages.put(newMessageUid, storedMessage); + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Internal error", e); + } + + return newMessageUid; + + } + + private void createMessageFiles(FileInfo messageHome, MimeBodyPart part) throws IOException, MessagingException + { + + Object content = part.getContent(); + + if (content instanceof MimeMultipart) + { + int count = ((MimeMultipart) content).getCount(); + for (int i = 0; i < count; i++) + { + createMessageFiles(messageHome, (MimeBodyPart) ((MimeMultipart) content).getBodyPart(i)); + } + } + else + { + + String partName = part.getFileName(); + if (partName == null) + { + processTextMessage(part, messageHome, true); + } + else + { + processTextMessage(part, messageHome, false); + } + + } + + } + + /** + * Copies message with the given UID to the specified {@link MailFolder}. + * + * @param uid - UID of the message + * @param toFolder - reference to the destination folder. + */ + public void copyMessage(long uid, MailFolder toFolder) throws FolderException + { + AlfrescoImapMailFolder toImapMailFolder = (AlfrescoImapMailFolder) toFolder; + + if (toImapMailFolder.isReadOnly()) + { + throw new FolderException("Can't create folder - Permission denied"); + } + + NodeRef toNodeRef = toImapMailFolder.getFolderInfo().getNodeRef(); + + SimpleStoredMessage message = messages.get(uid); + FileInfo copyMess = ((AlfrescoImapMessage) message.getMimeMessage()).getMessageInfo(); + + List fis = new LinkedList(); + + if (imapHelper.getNodeService().getType(copyMess.getNodeRef()).equals(ImapModel.TYPE_IMAP_CONTENT)) + { + + //TODO FILE EXIST + NodeRef messageFolder = imapHelper.getFileFolderService().create(toNodeRef, AlfrescoImapConst.MESSAGE_PREFIX + GUID.generate(), ImapModel.TYPE_IMAP_CONTENT).getNodeRef(); + + final long nextUid = (Long) imapHelper.getNodeService().getProperty(messageFolder, ContentModel.PROP_NODE_DBID); + + String name = AlfrescoImapConst.MESSAGE_PREFIX + nextUid; + try + { + imapHelper.getFileFolderService().rename(messageFolder, name); + } + catch (FileNotFoundException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + + Map srcMesProps = imapHelper.getNodeService().getProperties(copyMess.getNodeRef()); + Map dstMessProps = imapHelper.getNodeService().getProperties(messageFolder); + srcMesProps.putAll(dstMessProps); + imapHelper.getNodeService().setProperties(messageFolder, srcMesProps); + + + fis = imapHelper.getFileFolderService().search(copyMess.getNodeRef(), "*", false); + toNodeRef = messageFolder; + } + else + { + fis.add(copyMess); + } + for (FileInfo fi : fis) + { + try + { + imapHelper.getFileFolderService().copy(fi.getNodeRef(), toNodeRef, null); + } + catch (FileExistsException e) + { + logger.error(e); + } + catch (FileNotFoundException e) + { + logger.error(e); + } + } + } + + /** + * Marks all messages in the folder as deleted using {@link Flags.Flag#DELETED} flag. + */ + public void deleteAllMessages() throws FolderException + { + if (this.readOnly) + { + throw new FolderException("Can't delete all - Permission denied"); + } + + for (SimpleStoredMessage mess : messages.values()) + { + AlfrescoImapMessage message = (AlfrescoImapMessage) mess.getMimeMessage(); + FileInfo fileInfo = message.getMessageInfo(); + imapHelper.setFlag(fileInfo, Flags.Flag.DELETED, true); + // comment out to physically remove content. + // fileFolderService.delete(fileInfo.getNodeRef()); + messages.remove(mess.getUid()); + } + } + + /** + * Deletes messages marked with {@link Flags.Flag#DELETED}. Note that this message deletes all messages with this flag. + */ + public void expunge() throws FolderException + { + if (this.readOnly) + { + throw new FolderException("Can't expunge - Permission denied"); + } + + Collection listMess = messages.values(); + for (SimpleStoredMessage mess : listMess) + { + + Flags flags = getFlags(mess); + if (flags.contains(Flags.Flag.DELETED)) + { + NodeRef nodeRef = ((AlfrescoImapMessage) mess.getMimeMessage()).getMessageInfo().getNodeRef(); + imapHelper.getFileFolderService().delete(nodeRef); + } + } + } + + /** + * Returns the number of the first unseen message. + * + * @return Number of the first unseen message. + */ + public int getFirstUnseen() + { + return 0; + } + + /** + * Returns full name of the folder with namespace and full path delimited with the hierarchy delimiter (see {@link AlfrescoImapConst#HIERARCHY_DELIMITER})

E.g.:

+ * #mail.admin."Repository_archive.Data Dictionary.Space Templates.Software Engineering Project"

This is required by GreenMail implementation. + */ + public String getFullName() + { + + // If MailFolder object is used to obtain hierarchy delimiter by LIST command: + // Example: + // C: 2 list "" "" + // S: * LIST () "." "" + // S: 2 OK LIST completed. + + if (rootNodeRef == null) + { + return ""; + } + + StringBuilder fullName = new StringBuilder(); + List pathList; + try + { + pathList = imapHelper.getFileFolderService().getNamePath(rootNodeRef, folderInfo.getNodeRef()); + fullName.append(ImapConstants.USER_NAMESPACE).append(AlfrescoImapConst.HIERARCHY_DELIMITER).append(qualifiedMailboxName); + + boolean isFirst = true; + for (FileInfo path : pathList) + { + fullName.append(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (isFirst) + { + fullName.append("\""); + isFirst = false; + if (mountPointName != null) + { + fullName.append(mountPointName); + } + else + { + fullName.append(path.getName()); + } + } + else + { + fullName.append(path.getName()); + } + } + fullName.append("\""); + } + catch (FileNotFoundException e) + { + logger.error(e); + } + if (logger.isDebugEnabled()) + { + logger.debug("fullName: " + fullName); + } + return GreenMailUtil.convertInUtf7(fullName.toString()); + } + + /** + * Returns message by its UID. + * + * @param uid - UID of the message. + * @return message. + */ + public SimpleStoredMessage getMessage(long uid) + { + if (!isBodyGenerated) + { + // regenerate messages list and include message body into result + getMessages(); + } + return messages.get(uid); + } + + /** + * Returns count of the messages in the folder. + * + * @return Count of the messages. + */ + public int getMessageCount() + { + if (messages.size() == 0) + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + getMessages(fileInfos, false); + } + if (logger.isDebugEnabled()) + { + logger.debug(folderInfo.getName() + " - Messages count:" + messages.size()); + } + return messages.size(); + } + + /** + * Returns UIDs of all messages in the folder. + * + * @return UIDS of the messages. + */ + public long[] getMessageUids() + { + if (messages == null || messages.size() == 0) + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + getMessages(fileInfos, false); + } + int len = messages.size(); + long[] uids = new long[len]; + Set keys = messages.keySet(); + int i = 0; + for (Long key : keys) + { + uids[i++] = key; + } + return uids; + } + + /** + * Returns list of all messages in the folder. + * + * @return list of {@link SimpleStoredMessage} objects. + */ + public List getMessages() + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + return getMessages(fileInfos, true); + } + + private List getMessages(List fileInfos, boolean generateBody) + { + isBodyGenerated = generateBody; + if (fileInfos == null || fileInfos.size() == 0) + { + messages = Collections.emptyMap(); + } + if (fileInfos.size() != messages.size() || generateBody) + { + for (FileInfo fileInfo : fileInfos) + { + try + { + Long key = getMessageUid(fileInfo); + SimpleStoredMessage message = new SimpleStoredMessage(new AlfrescoImapMessage(fileInfo, imapHelper, generateBody), new Date(), key); + messages.put(key, message); + if (logger.isDebugEnabled()) + { + logger.debug("Message added: " + fileInfo.getName()); + } + } + catch (MessagingException e) + { + logger.warn("Invalid message! File name:" + fileInfo.getName(), e); + } + } + } + return new LinkedList(messages.values()); + } + + /** + * Returns list of messages by filter. + * + * @param msgRangeFilter - {@link MsgRangeFilter} object representing filter. + * @return list of filtered messages. + */ + public List getMessages(MsgRangeFilter msgRangeFilter) + { + if (messages == null || messages.size() == 0 || !isBodyGenerated) + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + getMessages(fileInfos, true); + } + List ret = new ArrayList(); + for (int i = 0; i < messages.size(); i++) + { + if (msgRangeFilter.includes(i + 1)) + { + ret.add(messages.get(i)); + } + } + + return ret; + } + + /** + * Returns message sequence number in the folder by its UID. + * + * @param uid - message UID. + * @return message sequence number. + * @throws FolderException if no message with given UID. + */ + public int getMsn(long uid) throws FolderException + { + // Um not sure in this because the getMsn is not documented... + // Implemented alike GreenMail implementation. + Set keys = messages.keySet(); + int msn = 0; + for (Long key : keys) + { + // "==" is legal with primitives and autoboxing + if (key == uid) + { + return msn + 1; + } + msn++; + } + throw new FolderException("No such message."); + } + + /** + * Returns folder name. + * + * @return folder name. + */ + public String getName() + { + return folderName; + } + + /** + * Returns the list of messages that have no {@link Flags.Flag#DELETED} flag set for current user. + * + * @return the list of non-deleted messages. + */ + public List getNonDeletedMessages() + { + List result = new ArrayList(); + + if (messages.size() == 0 || !isBodyGenerated) + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + getMessages(fileInfos, true); + } + + Collection values = messages.values(); + for (SimpleStoredMessage message : values) + { + if (!getFlags(message).contains(Flags.Flag.DELETED)) + { + result.add(message); + } + + } + if (logger.isDebugEnabled()) + { + logger.debug(folderInfo.getName() + " - Non deleted messages count:" + result.size()); + } + return result; + } + + /** + * Returns permanent flags. + * + * @return {@link Flags} object containing flags. + */ + public Flags getPermanentFlags() + { + return PERMANENT_FLAGS; + } + + /** + * Returns count of messages with {@link Flags.Flag#RECENT} flag. If {@code reset} parameter is {@code true} - removes {@link Flags.Flag#RECENT} flag from the message for + * current user. + * + * @param reset - if true the {@link Flags.Flag#RECENT} will be deleted for current user if exists. + * @return returns count of recent messages. + */ + public int getRecentCount(boolean reset) + { + if (messages.size() == 0) + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + getMessages(fileInfos, false); + } + + int count = 0; + Collection values = messages.values(); + for (SimpleStoredMessage message : values) + { + if (getFlags(message).contains(Flags.Flag.RECENT)) + { + count++; + if (reset) + { + imapHelper.setFlag(((AlfrescoImapMessage) message.getMimeMessage()).getMessageInfo(), Flags.Flag.RECENT, false); + } + } + + } + + if (logger.isDebugEnabled()) + { + logger.debug(folderInfo.getName() + " - Recent count: " + count + " reset: " + reset); + } + return count; + } + + /** + * Returns UIDNEXT value of the folder. + * + * @return UIDNEXT value. + */ + public long getUidNext() + { + return getUidValidity(); + } + + /** + * Returns UIDVALIDITY value of the folder. + * + * @return UIDVALIDITY value. + */ + public long getUidValidity() + { + return ((Date) imapHelper.getNodeService().getProperty(folderInfo.getNodeRef(), ContentModel.PROP_MODIFIED)).getTime(); + } + + /** + * Returns count of the messages with {@link Flags.Flag#SEEN} in the folder for the current user. + * + * @return Count of the unseen messages for current user. + */ + public int getUnseenCount() + { + if (messages.size() == 0) + { + List fileInfos = imapHelper.searchMails(folderInfo.getNodeRef(), "*", viewMode, false); + getMessages(fileInfos, false); + } + + int count = 0; + Collection values = messages.values(); + for (SimpleStoredMessage message : values) + { + if (!getFlags(message).contains(Flags.Flag.SEEN)) + { + count++; + } + + } + if (logger.isDebugEnabled()) + { + logger.debug(folderInfo.getName() + " - Unseen count: " + count); + } + return count; + } + + /** + * Removes {@link FolderListener} from the folder. + * + * @param listener - Listener to remove. + */ + public void removeListener(FolderListener listener) + { + listeners.remove(listener); + } + + /** + * Replaces flags for the message with the given UID. If {@code addUid} is set to {@code true} {@link FolderListener} objects defined for this folder will be notified. + * {@code silentListener} can be provided - this listener wouldn't be notified. + * + * @param flags - new flags. + * @param uid - message UID. + * @param silentListener - listener that shouldn't be notified. + * @param addUid - defines whether or not listeners be notified. + */ + public void replaceFlags(Flags flags, long uid, FolderListener silentListener, boolean addUid) throws FolderException + { + int msn = getMsn(uid); + SimpleStoredMessage message = messages.get(uid); + FileInfo fileInfo = ((AlfrescoImapMessage) message.getMimeMessage()).getMessageInfo(); + try + { + imapHelper.setFlags(fileInfo, MessageFlags.ALL_FLAGS, false); + imapHelper.setFlags(fileInfo, flags, true); + message = new SimpleStoredMessage(message.getMimeMessage(), message.getInternalDate(), uid); + messages.put(uid, message); + } + catch (MessagingException e) + { + logger.warn("Can't set flags due to an error:", e); + } + + Long uidNotification = addUid ? uid : null; + notifyFlagUpdate(msn, message.getFlags(), uidNotification, silentListener); + } + + private void notifyFlagUpdate(int msn, Flags flags, Long uidNotification, FolderListener silentListener) + { + synchronized (listeners) + { + for (FolderListener listener : listeners) + { + if (listener == silentListener) + { + continue; + } + + listener.flagsUpdated(msn, flags, uidNotification); + } + } + } + + /** + * Simply returns UIDs of all messages in the folder. + * + * @param searchTerm - not used + * @return UIDs of the messages + */ + public long[] search(SearchTerm searchTerm) + { + return getMessageUids(); + } + + /** + * Sets flags for the message with the given UID. If {@code addUid} is set to {@code true} {@link FolderListener} objects defined for this folder will be notified. + * {@code silentListener} can be provided - this listener wouldn't be notified. + * + * @param flags - new flags. + * @param value - flags value. + * @param uid - message UID. + * @param silentListener - listener that shouldn't be notified. + * @param addUid - defines whether or not listeners be notified. + */ + public void setFlags(Flags flags, boolean value, long uid, FolderListener silentListener, boolean addUid) throws FolderException + { + int msn = getMsn(uid); + SimpleStoredMessage message = (SimpleStoredMessage) messages.get(uid); + + try + { + imapHelper.setFlags(((AlfrescoImapMessage) message.getMimeMessage()).getMessageInfo(), flags, value); + message = new SimpleStoredMessage(message.getMimeMessage(), message.getInternalDate(), uid); + messages.put(uid, message); + } + catch (MessagingException e) + { + logger.warn("Can't set flags due to an error:", e); + } + + Long uidNotification = null; + if (addUid) + { + uidNotification = new Long(uid); + } + notifyFlagUpdate(msn, message.getFlags(), uidNotification, silentListener); + + } + + /** + * Method is called before the deletion of the folder. Notifies {@link FolderListener} objects with {@link FolderListener#mailboxDeleted()} method calls. + */ + public void signalDeletion() + { + synchronized (listeners) + { + for (int i = 0; i < listeners.size(); i++) + { + FolderListener listener = (FolderListener) listeners.get(i); + listener.mailboxDeleted(); + } + } + } + + /** + * Not supported. Added to implement {@link MailFolder#store(MovingMessage)}. + */ + public void store(MovingMessage mail) throws Exception + { + throw new UnsupportedOperationException("Method store(MovingMessage) is not suppoted."); + } + + /** + * Not supported. Added to implement {@link MailFolder#store(MimeMessage)}. + */ + public void store(MimeMessage message) throws Exception + { + throw new UnsupportedOperationException("Method store(MimeMessage) is not suppoted."); + } + + /** + * @param fileInfo - {@link FileInfo} representing message. + * @return UID of the message. + */ + private long getMessageUid(FileInfo fileInfo) + { + if (imapHelper.getNodeService().getType(fileInfo.getNodeRef()).equals(ContentModel.TYPE_FOLDER)) + { + return ((Date) imapHelper.getNodeService().getProperty(fileInfo.getNodeRef(), ContentModel.PROP_MODIFIED)).getTime(); + } + + return (Long) imapHelper.getNodeService().getProperty(fileInfo.getNodeRef(), ContentModel.PROP_NODE_DBID); + } + + private Flags getFlags(SimpleStoredMessage mess) + { + return ((AlfrescoImapMessage) mess.getMimeMessage()).getFlags(); + } + + // ----------------------Getters and Setters---------------------------- + + public FileInfo getFolderInfo() + { + return folderInfo; + } + + public void setFolderName(String folderName) + { + this.folderName = folderName; + } + + public void setViewMode(String viewMode) + { + this.viewMode = viewMode; + } + + public void setMountPointName(String mountPointName) + { + this.mountPointName = mountPointName; + } + + public void setMountParent(NodeRef mountParent) + { + this.rootNodeRef = mountParent; + } + + /** + * Whether the folder is selectable. + * + * @return {@code boolean}. + */ + public boolean isSelectable() + { + + return this.selectable; + } + + /** + * Sets {@link #selectable} property. + * + * @param selectable - {@code boolean}. + */ + public void setSelectable(boolean selectable) + { + this.selectable = selectable; + // Map properties = folderInfo.getProperties(); + // properties.put(ImapModel.PROP_IMAP_FOLDER_SELECTABLE, this.selectable); + // imapHelper.setProperties(folderInfo, properties); + } + + + /** + * Whether the folder is read-only for user. + * @return {@code boolean} + */ + public boolean isReadOnly() + { + return readOnly; + } + +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapMessage.java b/source/java/org/alfresco/repo/imap/AlfrescoImapMessage.java new file mode 100755 index 0000000000..1d2155586a --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapMessage.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import static org.alfresco.repo.imap.AlfrescoImapConst.BASE_64_ENCODING; +import static org.alfresco.repo.imap.AlfrescoImapConst.CONTENT_ID; +import static org.alfresco.repo.imap.AlfrescoImapConst.CONTENT_TRANSFER_ENCODING; +import static org.alfresco.repo.imap.AlfrescoImapConst.CONTENT_TYPE; +import static org.alfresco.repo.imap.AlfrescoImapConst.MIME_VERSION; +import static org.alfresco.repo.imap.AlfrescoImapConst.UTF_8; +import static org.alfresco.repo.imap.AlfrescoImapConst.X_ALF_NODEREF_ID; +import static org.alfresco.repo.imap.AlfrescoImapConst.X_ALF_SERVER_UID; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +import javax.mail.Address; +import javax.mail.Flags; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.internet.AddressException; +import javax.mail.internet.ContentType; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimeUtility; +import javax.mail.util.ByteArrayDataSource; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; +import org.alfresco.model.ImapModel; +import org.alfresco.repo.imap.ImapHelper.EmailBodyType; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Extended MimeMessage to represent a content stored in the Alfresco repository. + * + * @author Arseny Kovalchuk + */ +public class AlfrescoImapMessage extends MimeMessage +{ + /** Used if imapHelper.getDefaultFromAddress is not set */ + private static final String DEFAULT_EMAIL_FROM = "alfresco@alfresco.org"; + private static final String DEFAULT_EMAIL_TO = DEFAULT_EMAIL_FROM; + private static final String KOI8R_CHARSET = "koi8-r"; + + private static Log logger = LogFactory.getLog(AlfrescoImapMessage.class); + + private ImapHelper imapHelper; + private FileInfo messageInfo; + + /** + * Constructs {@link AlfrescoImapMessage} object. + * + * @param fileInfo - reference to the {@link FileInfo} object representing the message. + * @param imapHelper - reference to the {@link ImapHelper} object. + * @param generateBody - if {@code true} message body will be generated. + * + * @throws MessagingException if generation of the body fails. + */ + public AlfrescoImapMessage(FileInfo fileInfo, ImapHelper imapHelper, boolean generateBody) throws MessagingException + { + super(Session.getDefaultInstance(new Properties())); + this.messageInfo = fileInfo; + this.imapHelper = imapHelper; + if (generateBody) + { + setMessageHeaders(); + buildMessage(); + } + } + + /** + * Constructs {@link AlfrescoImapMessage} object. + * + * @param fileInfo - reference to the {@link FileInfo} object representing the message. + * @param imapHelper - reference to the {@link ImapHelper} object. + * @param message - {@link MimeMessage} + * @throws MessagingException + */ + public AlfrescoImapMessage(FileInfo fileInfo, ImapHelper imapHelper, MimeMessage message) throws MessagingException + { + super(message); + this.messageInfo = fileInfo; + this.imapHelper = imapHelper; + + setMessageHeaders(); + final NodeRef nodeRef = fileInfo.getNodeRef(); + Map props = new HashMap(); + props.put(ImapModel.PROP_MESSAGE_FROM, InternetAddress.toString(message.getFrom())); + props.put(ImapModel.PROP_MESSAGE_TO, InternetAddress.toString(message.getRecipients(RecipientType.TO))); + props.put(ImapModel.PROP_MESSAGE_CC, InternetAddress.toString(message.getRecipients(RecipientType.CC))); + + String[] subj = message.getHeader("Subject"); + if (subj.length > 0) + { + props.put(ImapModel.PROP_MESSAGE_SUBJECT, subj[0]); + imapHelper.getNodeService().setProperty(nodeRef, ContentModel.PROP_DESCRIPTION, subj[0]); + } + + Map allprops = imapHelper.getNodeService().getProperties(fileInfo.getNodeRef()); + allprops.putAll(props); + imapHelper.getNodeService().setProperties(nodeRef, allprops); + // setContent(buildMultipart(fileInfo)); - disabled for better performance. + } + + /** + * Returns message flags. + * + * @return {@link Flags} + */ + @Override + public synchronized Flags getFlags() + { + return imapHelper.getFlags(messageInfo); + } + + + /** + * Sets message flags. + * + * @param flags - {@link Flags} object. + * @param value - flags value. + */ + @Override + public synchronized void setFlags(Flags flags, boolean value) throws MessagingException + { + imapHelper.setFlags(messageInfo, flags, value); + } + + + /** + * Returns {@link FileInfo} object representing message in Alfresco. + * + * @return reference to the {@link FileInfo} object. + */ + public FileInfo getMessageInfo() + { + return messageInfo; + } + + private void setMessageHeaders() throws MessagingException + { + setHeader(MIME_VERSION, "1.0"); + // Optional headers for further implementation of multiple Alfresco server support. + setHeader(X_ALF_NODEREF_ID, messageInfo.getNodeRef().getId()); + setHeader(X_ALF_SERVER_UID, imapHelper.getAlfrescoServerUID()); + } + + /** + * This method builds MimeMessage based on either ImapModel or ContentModel type. + * + * @param fileInfo - Source file information {@link FileInfo} + * @throws MessagingException + */ + private void buildMessage() throws MessagingException + { + final NodeRef nodeRef = messageInfo.getNodeRef(); + if (ImapModel.TYPE_IMAP_CONTENT.equals(imapHelper.getNodeService().getType(nodeRef))) + { + buildImapModelMessage(); + } + else + { + buildContentModelMessage(); + } + } + + /** + * This method builds MimeMessage based on {@link ImapModel} + * + * @param fileInfo - Source file information {@link FileInfo} + * @throws MessagingException + */ + private void buildImapModelMessage() throws MessagingException + { + Map properties = messageInfo.getProperties(); + setSentDate(messageInfo.getModifiedDate()); + String prop = (String) properties.get(ImapModel.PROP_MESSAGE_FROM); + addFromInternal(prop); + prop = (String) properties.get(ImapModel.PROP_MESSAGE_TO); + + if (prop != null && prop.length() > 0) + { + addRecipients(RecipientType.TO, InternetAddress.parse(prop)); + } + else + { + addRecipients(RecipientType.TO, DEFAULT_EMAIL_TO); + } + + prop = (String) properties.get(ImapModel.PROP_MESSAGE_CC); + if (prop != null && prop.length() > 0) + { + addRecipients(RecipientType.CC, InternetAddress.parse(prop)); + } + + prop = (String) properties.get(ImapModel.PROP_MESSAGE_SUBJECT); + setSubject(prop == null ? messageInfo.getName() : prop); + + setContent(buildImapModelMultipart()); + + } + + /** + * This method builds {@link MimeMessage} based on {@link ContentModel} + * + * @param fileInfo - Source file information {@link FileInfo} + * @throws MessagingException + */ + private void buildContentModelMessage() throws MessagingException + { + Map properties = messageInfo.getProperties(); + String prop = null; + setSentDate(messageInfo.getModifiedDate()); + // Add FROM address + Address[] addressList = buildSenderFromAddress(properties); + addFrom(addressList); + // Add TO address + addressList = buildRecipientToAddress(); + addRecipients(RecipientType.TO, addressList); + prop = (String) properties.get(ContentModel.PROP_TITLE); + try + { + prop = (prop == null) ? MimeUtility.encodeText(messageInfo.getName(), KOI8R_CHARSET, null) : MimeUtility.encodeText(prop, KOI8R_CHARSET, null); + } + catch (UnsupportedEncodingException e) + { + // ignore + } + setSubject(prop); + setContent(buildContentModelMultipart()); + } + + /** + * This method builds {@link Multipart} based on {@link ContentModel} + * + * @param fileInfo - Source file information {@link FileInfo} + * @throws MessagingException + */ + private Multipart buildContentModelMultipart() throws MessagingException + { + MimeMultipart rootMultipart = new MimeMultipart("alternative"); + // Cite MOB-395: "email agent will be used to select an appropriate template" - we are not able to + // detect an email agent so we use a default template for all messages. + // See AlfrescoImapConst to see the possible templates to use. + String bodyTxt = imapHelper.getEmailBodyText(messageInfo.getNodeRef(), EmailBodyType.TEXT_PLAIN); + rootMultipart.addBodyPart(getTextBodyPart(bodyTxt, EmailBodyType.TEXT_PLAIN.getSubtype())); + String bodyHtml = imapHelper.getEmailBodyText(messageInfo.getNodeRef(), EmailBodyType.TEXT_HTML); + rootMultipart.addBodyPart(getTextBodyPart(bodyHtml, EmailBodyType.TEXT_HTML.getSubtype())); + return rootMultipart; + } + + private MimeBodyPart getTextBodyPart(String bodyText, String subtype) throws MessagingException + { + MimeBodyPart result = new MimeBodyPart(); + result.setText(bodyText, UTF_8, subtype); + result.addHeader(CONTENT_TRANSFER_ENCODING, BASE_64_ENCODING); + return result; + } + + /** + * This method builds {@link Multipart} based on {@link ImapModel} + * + * @param fileInfo - Source file information {@link FileInfo} + * @throws MessagingException + */ + private Multipart buildImapModelMultipart() throws MessagingException + { + DataSource source = null; + String errorMessage = null; + + // Root multipart - multipart/mixed + MimeMultipart rootMultipart = new MimeMultipart("mixed"); + // Message body - multipart/alternative - consists of two parts: text/plain and text/html + MimeMultipart messageBody = new MimeMultipart("alternative"); + // <------------------------ text html body part ------------------------> + List bodyHtmls = imapHelper.searchFiles(messageInfo.getNodeRef(), "*.html", ImapModel.TYPE_IMAP_BODY, false); + ContentType contentType = null; + MimeBodyPart textHtmlBodyPart = null; + if (bodyHtmls != null && bodyHtmls.size() > 0) + { + textHtmlBodyPart = new MimeBodyPart(); + FileInfo bodyHtml = bodyHtmls.get(0); + contentType = new ContentType(bodyHtml.getContentData().getMimetype()); + ContentReader reader = imapHelper.getFileFolderService().getReader(bodyHtml.getNodeRef()); + try + { + source = new ByteArrayDataSource(reader.getContentInputStream(), contentType.toString()); + } + catch (IOException e) + { + logger.error(e); + errorMessage = e.getMessage(); + } + if (source != null) + { + textHtmlBodyPart.setDataHandler(new DataHandler(source)); + textHtmlBodyPart.addHeader(CONTENT_TYPE, bodyHtml.getContentData().getMimetype()); + // textHtmlBodyPart.addHeader(CONTENT_TRANSFER_ENCODING, EIGHT_BIT_ENCODING); + textHtmlBodyPart.addHeader(CONTENT_TRANSFER_ENCODING, BASE_64_ENCODING); + } + else + { + textHtmlBodyPart.setText(errorMessage, UTF_8); + } + messageBody.addBodyPart(textHtmlBodyPart); + } + // + // <------------------------ text plain body part ------------------------> + List results = imapHelper.searchFiles(messageInfo.getNodeRef(), "*.txt", ImapModel.TYPE_IMAP_BODY, false); + MimeBodyPart textPlainBodyPart = null; + String text = null; + if (results != null && results.size() > 0) + { + textPlainBodyPart = new MimeBodyPart(); + FileInfo bodyTxt = results.get(0); + text = imapHelper.getFileFolderService().getReader(bodyTxt.getNodeRef()).getContentString(); + contentType = new ContentType(bodyTxt.getContentData().getMimetype()); + } + else if (textHtmlBodyPart == null) + { + text = I18NUtil.getMessage("imap.server.info.message_body_not_found"); + contentType = new ContentType(EmailBodyType.TEXT_PLAIN.getMimeType() + "; charset=UTF-8"); + } + + textPlainBodyPart.setText(text, contentType.getParameter("charset"), contentType.getSubType()); + textPlainBodyPart.addHeader(CONTENT_TYPE, contentType.toString()); + messageBody.addBodyPart(textPlainBodyPart); + // + + // Body part for multipart/alternative + MimeBodyPart messageBodyPart = new MimeBodyPart(); + messageBodyPart.setContent(messageBody); + // Add multipart/alternative into root multipart/mixed... + rootMultipart.addBodyPart(messageBodyPart); + + // Process attachments + List attaches = imapHelper.searchFiles(messageInfo.getNodeRef(), "*", ImapModel.TYPE_IMAP_ATTACH, false); + + for (FileInfo attach : attaches) + { + try + { + + errorMessage = null; + messageBodyPart = new MimeBodyPart(); + ContentReader reader = imapHelper.getFileFolderService().getReader(attach.getNodeRef()); + source = new ByteArrayDataSource(reader.getContentInputStream(), attach.getContentData().getMimetype()); + } + catch (IOException e) + { + logger.error(e); + errorMessage = e.getMessage(); + } + if (source != null) + { + String attachID = (String) imapHelper.getNodeService().getProperty(attach.getNodeRef(), ImapModel.PROP_ATTACH_ID); + if (attachID != null) + { + messageBodyPart.addHeader(CONTENT_ID, attachID); + } + StringBuilder ct = new StringBuilder(attach.getContentData().getMimetype()).append("; name=\"").append(attach.getName()).append("\""); + messageBodyPart.addHeader(CONTENT_TYPE, ct.toString()); + messageBodyPart.addHeader(CONTENT_TRANSFER_ENCODING, BASE_64_ENCODING); + messageBodyPart.setDataHandler(new DataHandler(source)); + try + { + messageBodyPart.setFileName(MimeUtility.encodeText(attach.getName(), KOI8R_CHARSET, null)); + } + catch (UnsupportedEncodingException e) + { + // ignore + } + } + else + { + messageBodyPart.setText(errorMessage, UTF_8); + } + rootMultipart.addBodyPart(messageBodyPart); + } + return rootMultipart; + } + + private void addFromInternal(String addressesString) throws MessagingException + { + if (addressesString != null) + { + addFrom(InternetAddress.parse(addressesString)); + } + else + { + addFrom(new Address[] { new InternetAddress(DEFAULT_EMAIL_FROM) }); + } + } + + /** + * TODO USE CASE 2: "The To/addressee will be the first email alias found in the parent folders or a default one (TBD)". It seems to be more informative as alike + * {@code @}... + * + * @return Generated TO address {@code @} + * @throws AddressException + */ + private InternetAddress[] buildRecipientToAddress() throws AddressException + { + InternetAddress[] result = null; + String defaultEmailTo = null; + // TODO : search first email alias found in the parent folders + // if (found) defaultEmailTo = foundAlias + // else + final String escapedUserName = imapHelper.getCurrentUser().replaceAll("[/,\\,@]", "."); + final String userDomain = DEFAULT_EMAIL_TO.split("@")[1]; + defaultEmailTo = escapedUserName + "@" + userDomain; + try + { + result = InternetAddress.parse(defaultEmailTo); + } + catch (AddressException e) + { + logger.error(String.format("Wrong email address '%s'.", defaultEmailTo), e); + result = InternetAddress.parse(DEFAULT_EMAIL_TO); + } + return result; + } + + /** + * Builds the InternetAddress from the Content Author name if provided. If name not specified, it takes Content Creator name. If content creator does not exists, the default + * from address will be returned. + * + * @param contentAuthor The content author full name. + * @return Generated InternetAddress[] array. + * @throws AddressException + */ + private InternetAddress[] buildSenderFromAddress(Map properties) throws AddressException + { + // Generate FROM address (Content author) + InternetAddress[] addressList = null; + String prop = (String) properties.get(ContentModel.PROP_AUTHOR); + String defaultFromAddress = imapHelper.getDefaultFromAddress(); + defaultFromAddress = defaultFromAddress == null ? DEFAULT_EMAIL_FROM : defaultFromAddress; + try + { + + if (prop != null) + { + StringBuilder contentAuthor = new StringBuilder(); + contentAuthor.append("\"").append(prop).append("\" <").append(defaultFromAddress).append(">"); + addressList = InternetAddress.parse(contentAuthor.toString()); + } + else + { + prop = (String) properties.get(ContentModel.PROP_CREATOR); + if (prop != null) + { + StringBuilder creator = new StringBuilder(); + creator.append("\"").append(prop).append("\" <").append(defaultFromAddress).append(">"); + addressList = InternetAddress.parse(creator.toString()); + } + else + { + throw new AddressException(I18NUtil.getMessage("imap.server.error.properties_dont_exist")); + } + } + } + catch (AddressException e) + { + addressList = InternetAddress.parse(DEFAULT_EMAIL_FROM); + } + return addressList; + } + +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java b/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java new file mode 100755 index 0000000000..1efdeb32fa --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapServer.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import org.alfresco.util.AbstractLifecycleBean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +import com.icegreen.greenmail.Managers; +import com.icegreen.greenmail.imap.ImapHostManager; +import com.icegreen.greenmail.imap.ImapServer; +import com.icegreen.greenmail.user.UserManager; +import com.icegreen.greenmail.util.ServerSetup; + +/** + * @author Mike Shavnev + */ +public class AlfrescoImapServer extends AbstractLifecycleBean +{ + + private static Log logger = LogFactory.getLog(AlfrescoImapServer.class); + + private ImapServer serverImpl; + + private int port = 143; + + private ImapHostManager imapHostManager; + + private UserManager imapUserManager; + + private boolean imapServerEnabled; + + private ImapHelper imapHelper; + + public void setImapServerEnabled(boolean imapServerEnabled) + { + this.imapServerEnabled = imapServerEnabled; + } + + public void setPort(int port) + { + this.port = port; + } + + public void setImapHostManager(ImapHostManager imapHostManager) + { + this.imapHostManager = imapHostManager; + } + + public void setImapUserManager(UserManager imapUserManager) + { + this.imapUserManager = imapUserManager; + } + + public void setImapHelper(ImapHelper imapHelper) + { + this.imapHelper = imapHelper; + } + + protected void onBootstrap(ApplicationEvent event) + { + if (imapServerEnabled && imapHelper.isPatchApplied()) + { + Managers imapManagers = new Managers() + { + public ImapHostManager getImapHostManager() + { + return imapHostManager; + } + + public UserManager getUserManager() + { + return imapUserManager; + } + }; + serverImpl = new ImapServer(new ServerSetup(port, null, ServerSetup.PROTOCOL_IMAP), imapManagers); + serverImpl.startService(null); + if (logger.isInfoEnabled()) + { + logger.info("IMAP service started on port " + this.port + "."); + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("IMAP service is disabled."); + } + } + } + + protected void onShutdown(ApplicationEvent event) + { + if (serverImpl != null) + { + serverImpl.stopService(null); + } + } + +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java b/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java new file mode 100755 index 0000000000..ed5d31aa71 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapUser.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import javax.mail.internet.MimeMessage; + +import com.icegreen.greenmail.imap.ImapHostManager; +import com.icegreen.greenmail.mail.MovingMessage; +import com.icegreen.greenmail.user.GreenMailUser; +import com.icegreen.greenmail.user.UserException; + +/** + * Alfresco implementation of the GreenMailUser interface. + * + * @author Arseny Kovalchuk + */ +public class AlfrescoImapUser implements GreenMailUser +{ + private String userName; + private char[] password; + private String email; + + private ImapHostManager imapHostManager; + + public AlfrescoImapUser(String email, String login, String password, ImapHostManager imapHostManager) + { + this.email = email; + this.userName = login; + this.password = password.toCharArray(); + this.imapHostManager = imapHostManager; + } + + public void authenticate(String password) throws UserException + { + throw new UnsupportedOperationException(); + // This method is used in the POP3 greenmail implementation, so it is disabled for IMAP + // See AlfrescoImapUserManager.test() method. + } + + public void create() throws UserException + { + throw new UnsupportedOperationException(); + } + + public void delete() throws UserException + { + throw new UnsupportedOperationException(); + } + + public void deliver(MovingMessage msg) throws UserException + { + + } + + public void deliver(MimeMessage msg) throws UserException + { + + } + + public String getEmail() + { + return this.email; + } + + public String getLogin() + { + return this.userName; + } + + public String getPassword() + { + return new String(this.password); + } + + public String getQualifiedMailboxName() + { + return userName; + } + + public void setPassword(String password) + { + this.password = password.toCharArray(); + } + +} diff --git a/source/java/org/alfresco/repo/imap/AlfrescoImapUserManager.java b/source/java/org/alfresco/repo/imap/AlfrescoImapUserManager.java new file mode 100755 index 0000000000..eb938cb728 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/AlfrescoImapUserManager.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.icegreen.greenmail.imap.ImapHostManager; +import com.icegreen.greenmail.user.GreenMailUser; +import com.icegreen.greenmail.user.UserException; +import com.icegreen.greenmail.user.UserManager; + +/** + * @author Arseny Kovalchuk + */ +public class AlfrescoImapUserManager extends UserManager +{ + private Log logger = LogFactory.getLog(AlfrescoImapUserManager.class); + + protected Map userMap = Collections.synchronizedMap(new HashMap()); + protected ImapHostManager imapHostManager; + + protected AuthenticationService authenticationService; + protected PersonService personService; + protected NodeService nodeService; + + public AlfrescoImapUserManager() + { + super(null); + } + + public AlfrescoImapUserManager(ImapHostManager imapHostManager) + { + this(); + this.imapHostManager = imapHostManager; + } + + public GreenMailUser createUser(String email, String login, String password) throws UserException + { + // TODO: User creation/addition code should be implemented here (in the AlfrescoImapUserManager). + // Following code is not need and not used in the current implementation. + GreenMailUser user = new AlfrescoImapUser(email, login, password, imapHostManager); + user.create(); + addUser(user); + return user; + } + + protected void addUser(GreenMailUser user) + { + userMap.put(user.getLogin(), user); + } + + public GreenMailUser getUser(String login) + { + return (GreenMailUser) userMap.get(login); + } + + public GreenMailUser getUserByEmail(String email) + { + GreenMailUser ret = getUser(email); + if (null == ret) + { + for (GreenMailUser user : userMap.values()) + { + // TODO: NPE! + if (user.getEmail().trim().equalsIgnoreCase(email.trim())) + { + return user; + } + } + } + return ret; + } + + public void deleteUser(GreenMailUser user) throws UserException + { + user = (GreenMailUser) userMap.remove(user.getLogin()); + if (user != null) + { + user.delete(); + } + } + + /** + * The login method. + * + * @see com.icegreen.greenmail.imap.commands.LoginCommand#doProcess() + */ + public boolean test(String userid, String password) + { + try + { + authenticationService.authenticate(userid, password.toCharArray()); + String email = null; + if (personService.personExists(userid)) + { + NodeRef personNodeRef = personService.getPerson(userid); + email = (String) nodeService.getProperty(personNodeRef, ContentModel.PROP_EMAIL); + } + GreenMailUser user = new AlfrescoImapUser(email, userid, password, imapHostManager); + addUser(user); + } + catch (AuthenticationException ex) + { + logger.error("IMAP authentication failed for userid: " + userid); + return false; + } + return true; + } + + public ImapHostManager getImapHostManager() + { + return this.imapHostManager; + } + + public void setImapHostManager(ImapHostManager imapHostManager) + { + this.imapHostManager = imapHostManager; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + +} diff --git a/source/java/org/alfresco/repo/imap/ImapHelper.java b/source/java/org/alfresco/repo/imap/ImapHelper.java new file mode 100755 index 0000000000..3326cd9be3 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/ImapHelper.java @@ -0,0 +1,980 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap; + +import static org.alfresco.repo.imap.AlfrescoImapConst.CLASSPATH_TEXT_HTML_TEMPLATE; +import static org.alfresco.repo.imap.AlfrescoImapConst.CLASSPATH_TEXT_PLAIN_TEMPLATE; +import static org.alfresco.repo.imap.AlfrescoImapConst.DICTIONARY_TEMPLATE_PREFIX; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.mail.Flags; +import javax.mail.Flags.Flag; + +import org.alfresco.config.Config; +import org.alfresco.config.ConfigService; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.model.ImapModel; +import org.alfresco.repo.admin.patch.PatchInfo; +import org.alfresco.repo.admin.patch.PatchService; +import org.alfresco.repo.imap.config.ImapConfigElement; +import org.alfresco.repo.imap.config.ImapConfigElement.ImapConfig; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.repo.template.TemplateNode; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.preference.PreferenceService; +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.repository.TemplateService; +import org.alfresco.service.cmr.search.ResultSet; +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.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.AbstractLifecycleBean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +/** + * Helper class to access repository services by IMAP components. Also contains a common helper methods to search and manage IMAP content and other usefull methods. Configured as + * {@code } in the {@code imap-server-context.xml} file. + * + * @author Dmitry Vaserin + */ +public class ImapHelper extends AbstractLifecycleBean +{ + private static Log logger = LogFactory.getLog(ImapHelper.class); + + private static String PATCH_ID = "patch.imapFolders"; + + private NodeService nodeService; + private SearchService searchService; + private FileFolderService fileFolderService; + private TemplateService templateService; + private NamespaceService namespaceService; + private PermissionService permissionService; + private ConfigService configService; + private DictionaryService dictionaryService; + private PreferenceService preferenceService; + private SiteService siteService; + + private ServiceRegistry serviceRegistry; + + private PatchService patchService; + + private String defaultFromAddress; + private String webApplicationContextUrl = "http://localhost:8080/alfresco"; + private String repositoryTemplatePath; + private String imapRoot; + private NodeRef spacesStoreNodeRef; + private NodeRef companyHomeNodeRef; + private NodeRef imapRootNodeRef; + + private boolean patchApplied = false; + + private final static Map qNameToFlag; + private final static Map flagToQname; + + static + { + qNameToFlag = new HashMap(); + qNameToFlag.put(ImapModel.PROP_FLAG_ANSWERED, Flags.Flag.ANSWERED); + qNameToFlag.put(ImapModel.PROP_FLAG_DELETED, Flags.Flag.DELETED); + qNameToFlag.put(ImapModel.PROP_FLAG_DRAFT, Flags.Flag.DRAFT); + qNameToFlag.put(ImapModel.PROP_FLAG_SEEN, Flags.Flag.SEEN); + qNameToFlag.put(ImapModel.PROP_FLAG_RECENT, Flags.Flag.RECENT); + qNameToFlag.put(ImapModel.PROP_FLAG_FLAGGED, Flags.Flag.FLAGGED); + + flagToQname = new HashMap(); + flagToQname.put(Flags.Flag.ANSWERED, ImapModel.PROP_FLAG_ANSWERED); + flagToQname.put(Flags.Flag.DELETED, ImapModel.PROP_FLAG_DELETED); + flagToQname.put(Flags.Flag.DRAFT, ImapModel.PROP_FLAG_DRAFT); + flagToQname.put(Flags.Flag.SEEN, ImapModel.PROP_FLAG_SEEN); + flagToQname.put(Flags.Flag.RECENT, ImapModel.PROP_FLAG_RECENT); + flagToQname.put(Flags.Flag.FLAGGED, ImapModel.PROP_FLAG_FLAGGED); + } + + public static enum EmailBodyType + { + TEXT_PLAIN, TEXT_HTML; + + public String getSubtype() + { + return name().toLowerCase().substring(5); + } + + public String getTypeSubtype() + { + return name().toLowerCase().replaceAll("_", ""); + } + + public String getMimeType() + { + return name().toLowerCase().replaceAll("_", "/"); + } + + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // Do nothing + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Void doWork() throws Exception + { + List patches = getPatchService().getPatches(null, null); + for (PatchInfo patch : patches) + { + if (patch.getId().equals(PATCH_ID)) + { + patchApplied = true; + break; + } + } + + if (!patchApplied) + { + return null; + } + + int indexOfStoreDelim = imapRoot.indexOf(StoreRef.URI_FILLER); + + if (indexOfStoreDelim == -1) + { + throw new RuntimeException("Bad path format, " + StoreRef.URI_FILLER + " not found"); + } + + indexOfStoreDelim += StoreRef.URI_FILLER.length(); + + int indexOfPathDelim = imapRoot.indexOf("/", indexOfStoreDelim); + + if (indexOfPathDelim == -1) + { + throw new java.lang.RuntimeException("Bad path format, / not found"); + } + + String storePath = imapRoot.substring(0, indexOfPathDelim); + String rootPathInStore = imapRoot.substring(indexOfPathDelim); + + StoreRef storeRef = new StoreRef(storePath); + + if (nodeService.exists(storeRef) == false) + { + throw new RuntimeException("No store for path: " + storeRef); + } + + NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); + + List nodeRefs = searchService.selectNodes(storeRootNodeRef, rootPathInStore, null, namespaceService, false); + + if (nodeRefs.size() > 1) + { + throw new RuntimeException("Multiple possible roots for : \n" + " root path: " + rootPathInStore + "\n" + " results: " + nodeRefs); + } + else if (nodeRefs.size() == 0) + { + throw new RuntimeException("No root found for : \n" + " root path: " + rootPathInStore); + } + + imapRootNodeRef = nodeRefs.get(0); + + // Get "Company Home" node reference + StoreRef store = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + ResultSet rs = searchService.query(store, SearchService.LANGUAGE_XPATH, "/app:company_home"); + try + { + if (rs.length() == 0) + { + throw new AlfrescoRuntimeException("'Company Home' space doesn't exists."); + } + companyHomeNodeRef = rs.getNodeRef(0); + } + finally + { + rs.close(); + } + + spacesStoreNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Search for files in specified context + * + * @param contextNodeRef context folder for search + * @param namePattern name pattern for search + * @param searchType type for search + * @param includeSubFolders include SubFolders + * @return list of files with specifed type + */ + public List searchFiles(NodeRef contextNodeRef, String namePattern, QName searchType, boolean includeSubFolders) + { + return search(contextNodeRef, namePattern, searchType, true, false, includeSubFolders); + } + + /** + * Search for mailboxes in specified context + * + * @param contextNodeRef context folder for search + * @param namePattern name pattern for search + * @param includeSubFolders include SubFolders + * @param isVirtualView is folder in "Virtual" View + * @return list of mailboxes + */ + public List searchFolders(NodeRef contextNodeRef, String namePattern, boolean includeSubFolders, boolean isVirtualView) + { + QName searchType = ContentModel.TYPE_FOLDER; + if (isVirtualView) + { + searchType = null; + } + + List result = search(contextNodeRef, namePattern, searchType, false, true, includeSubFolders); + if (isVirtualView) + { + List nonFavSites = getNonFavouriteSites(getCurrentUser()); + for (SiteInfo siteInfo : nonFavSites) + { + FileInfo nonFavSite = fileFolderService.getFileInfo(siteInfo.getNodeRef()); + List siteChilds = search(nonFavSite.getNodeRef(), namePattern, null, false, true, true); + result.removeAll(siteChilds); + result.remove(nonFavSite); + } + + } + else + { + // Remove folders from Sites + List sites = siteService.listSites(getCurrentUser()); + for (SiteInfo siteInfo : sites) + { + List siteChilds = search(siteInfo.getNodeRef(), namePattern, null, false, true, true); + result.removeAll(siteChilds); + } + + } + return result; + } + + /** + * Search for emails in specified folder depend on view mode. + * + * @param contextNodeRef context folder for search + * @param namePattern name pattern for search + * @param viewMode context folder view mode + * @param includeSubFolders includeSubFolders + * @return list of emails that context folder contains. + */ + public List searchMails(NodeRef contextNodeRef, String namePattern, String viewMode, boolean includeSubFolders) + { + + List result = new LinkedList(); + if (viewMode.equals(AlfrescoImapConst.MODE_ARCHIVE)) + { + result = search(contextNodeRef, namePattern, ImapModel.TYPE_IMAP_CONTENT, false, true, includeSubFolders); + } + else + { + if (viewMode.equals(AlfrescoImapConst.MODE_VIRTUAL)) + { + result = search(contextNodeRef, namePattern, null, true, false, includeSubFolders); + } + } + + return result; + } + + private List search(NodeRef contextNodeRef, String namePattern, QName searchType, boolean fileSearch, boolean folderSearch, boolean includeSubFolders) + { + List result = new LinkedList(); + List searchResult = fileFolderService.search(contextNodeRef, namePattern, fileSearch, folderSearch, includeSubFolders); + + if (searchType == null) + { + return searchResult; + } + + for (FileInfo fileInfo : searchResult) + { + if (nodeService.getType(fileInfo.getNodeRef()).equals(searchType)) + { + result.add(fileInfo); + } + } + + return result; + } + + /** + * Get root reference for the specified mailbox + * + * @param mailboxName mailbox name in IMAP client. + * @param userName + * @return + */ + public NodeRef getMailboxRootRef(String mailboxName, String userName) + { + String rootFolder; + int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (index > 0) + { + rootFolder = mailboxName.substring(0, index); + } + else + { + rootFolder = mailboxName; + } + + Map imapConfigs = getImapConfigs(); + if (imapConfigs.keySet().contains(rootFolder)) + { + Map mountPoints = getMountPoints(); + NodeRef mountRef = mountPoints.get(rootFolder); + return nodeService.getParentAssocs(mountRef).get(0).getParentRef(); + } + else + { + return getUserImapHomeRef(userName); + } + } + + /** + * @param userName user name + * @return user IMAP home reference + */ + public NodeRef getUserImapHomeRef(String userName) + { + return fileFolderService.searchSimple(imapRootNodeRef, userName); + } + + public String getCurrentUser() + { + return AuthenticationUtil.getFullyAuthenticatedUser(); + } + + public String getUserImapHomeId(String userName) + { + return getUserImapHomeRef(userName).getId(); + } + + public NodeRef getImapRootNodeRef() + { + return imapRootNodeRef; + } + + public NodeRef getCompanyHomeNodeRef() + { + return companyHomeNodeRef; + } + + public NodeRef getSpacesStoreNodeRef() + { + return spacesStoreNodeRef; + } + + public void setImapRoot(String imapRoot) + { + this.imapRoot = imapRoot; + } + + public String getDefaultFromAddress() + { + return defaultFromAddress; + } + + public void setDefaultFromAddress(String defaultFromAddress) + { + this.defaultFromAddress = defaultFromAddress; + } + + public String getWebApplicationContextUrl() + { + return this.webApplicationContextUrl; + } + + public void setWebApplicationContextUrl(String webApplicationContextUrl) + { + this.webApplicationContextUrl = webApplicationContextUrl; + } + + public String getRepositoryTemplatePath() + { + return repositoryTemplatePath; + } + + public void setRepositoryTemplatePath(String repositoryTemplatePath) + { + this.repositoryTemplatePath = repositoryTemplatePath; + } + + /** + * 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(); + checkForFlaggableAspect(messageInfo.getNodeRef()); + Map props = nodeService.getProperties(messageInfo.getNodeRef()); + + for (QName key : qNameToFlag.keySet()) + { + Boolean value = (Boolean) props.get(key); + if (value != null && value) + { + flags.add(qNameToFlag.get(key)); + } + } + // This is a multiuser flag support. Commented due new requirements + // for (QName key : qNameToFlag.keySet()) + // { + // if (key.equals(ImapModel.PROP_FLAG_DELETED)) + // { + // Boolean value = (Boolean) props.get(key); + // if (value != null && value) + // { + // flags.add(qNameToFlag.get(key)); + // } + // } + // else + // { + // String users = (String) props.get(key); + // + // if (users != null && users.indexOf(formatUserEntry(getCurrentUser())) >= 0) + // { + // flags.add(qNameToFlag.get(key)); + // } + // } + // } + + return flags; + } + + /** + * Set flags to the specified imapFolder. + * + * @param messageInfo FileInfo of imap Folder. + * @param flags flags to set. + * @param value value to set. + */ + public void setFlags(FileInfo messageInfo, Flags flags, boolean value) + { + checkForFlaggableAspect(messageInfo.getNodeRef()); + for (Flags.Flag flag : flags.getSystemFlags()) + { + setFlag(messageInfo, flag, value); + } + } + + /** + * Set flags to the specified imapFolder. + * + * @param messageInfo FileInfo of imap Folder + * @param flag flag to set. + * @param value value value to set. + */ + public void setFlag(final FileInfo messageInfo, final Flag flag, final boolean value) + { + checkForFlaggableAspect(messageInfo.getNodeRef()); + nodeService.setProperty(messageInfo.getNodeRef(), flagToQname.get(flag), value); + + // This is a multiuser flag support. Commented due new requirements + // if (flagToQname.get(flag).equals(ImapModel.PROP_FLAG_DELETED)) + // { + // nodeService.setProperty(messageInfo.getNodeRef(), flagToQname.get(flag), value); + // } + // else + // { + // AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + // { + // public Void doWork() throws Exception + // { + // + // String users = (String) nodeService.getProperty(messageInfo.getNodeRef(), flagToQname.get(flag)); + // if (value) + // { + // if (users == null) + // { + // users = ""; + // } + // users += formatUserEntry(getCurrentUser()); + // + // } + // else if (users != null) + // { + // users = users.replace(formatUserEntry(getCurrentUser()), ""); + // + // } + // nodeService.setProperty(messageInfo.getNodeRef(), flagToQname.get(flag), users); + // return null; + // } + // }, AuthenticationUtil.getSystemUserName()); + // } + + } + + /** + * Check that the given authentication has a particular permission for the given node. + * + * @param nodeRef nodeRef of the node + * @param permission permission for check + * @return the access status + */ + public AccessStatus hasPermission(NodeRef nodeRef, String permission) + { + return permissionService.hasPermission(nodeRef, permission); + } + + /** + * Change userName into following format ;userName; + * + * @param userName + * @return + */ + public String formatUserEntry(String userName) + { + return AlfrescoImapConst.USER_SEPARATOR + userName + AlfrescoImapConst.USER_SEPARATOR; + } + + /** + * 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"; + } + + /** + * Map of mount points. Name of mount point == key in the map. + * + * @return Map of mount points. + */ + public Map getMountPoints() + { + Map imapConfigs = getImapConfigs(); + Map mountPoints = new HashMap(); + + for (ImapConfig config : imapConfigs.values()) + { + // Get node reference + StoreRef store = new StoreRef(config.getStore()); + ResultSet rs = searchService.query(store, SearchService.LANGUAGE_XPATH, config.getRootPath()); + if (rs.length() == 0) + { + logger.warn("Didn't find " + config.getName()); + } + else + { + NodeRef nodeRef = rs.getNodeRef(0); + mountPoints.put(config.getName(), nodeRef); + } + rs.close(); + } + return mountPoints; + } + + /** + * Return map of imap configs. Name of config == key in the map + * + * @return map of imap configs. + */ + public Map getImapConfigs() + { + Config imapConfig = configService.getConfig("imapConfig"); + ImapConfigElement imapConfigElement = (ImapConfigElement) imapConfig.getConfigElement(ImapConfigElement.CONFIG_ELEMENT_ID); + return imapConfigElement.getImapConfigs(); + } + + /** + * Return view mode ("virtual" or "archive") for specified mailbox. + * + * @param mailboxName name of the mailbox in IMAP client. + * @return view mode of the specified mailbox. + */ + public String getViewMode(String mailboxName) + { + String rootFolder; + int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (index > 0) + { + rootFolder = mailboxName.substring(0, index); + } + else + { + rootFolder = mailboxName; + } + Map imapConfigs = getImapConfigs(); + if (imapConfigs.keySet().contains(rootFolder)) + { + return imapConfigs.get(rootFolder).getMode(); + } + else + { + return AlfrescoImapConst.MODE_ARCHIVE; + } + } + + /** + * Return mount point name, which was specified in imap-config.xml for the current mailbox. + * + * @param mailboxName mailbox name in IMAP client. + * @return mount point name or null. + */ + public String getMountPointName(String mailboxName) + { + String rootFolder; + int index = mailboxName.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (index > 0) + { + rootFolder = mailboxName.substring(0, index); + } + else + { + rootFolder = mailboxName; + } + Map imapConfigs = getImapConfigs(); + if (imapConfigs.keySet().contains(rootFolder)) + { + return rootFolder; + } + else + { + return null; + } + + } + + /** + * Convert mailpath from IMAP client representation to the alfresco representation view. (e.g. with default settings "getMailPathInRepo(Repository_virtual.Imap Home)" will + * return "Company Home.Imap Home") + * + * @param mailPath mailbox path in IMAP client + * @return mailbox path in alfresco + */ + public String getMailPathInRepo(String mailPath) + { + String rootFolder; + String remain = ""; + int index = mailPath.indexOf(AlfrescoImapConst.HIERARCHY_DELIMITER); + if (index > 0) + { + rootFolder = mailPath.substring(0, index); + remain = mailPath.substring(index); + } + else + { + rootFolder = mailPath; + } + Map imapConfigs = getImapConfigs(); + if (imapConfigs.keySet().contains(rootFolder)) + { + Map mountPoints = getMountPoints(); + NodeRef rootRef = mountPoints.get(rootFolder); + String rootName = nodeService.getProperty(rootRef, ContentModel.PROP_NAME).toString(); + + return rootName + remain; + } + else + { + return mailPath; + } + } + + /** + * Return list of sites, that belong to the specified user and not marked as "Imap favourite" + * + * @param userName name of user + * @return List of nonFavourite sites. + */ + public List getNonFavouriteSites(String userName) + { + List nonFavSites = new LinkedList(); + Map prefs = preferenceService.getPreferences(userName, AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES); + List sites = siteService.listSites(userName); + for (SiteInfo siteInfo : sites) + { + String key = AlfrescoImapConst.PREF_IMAP_FAVOURITE_SITES + "." + siteInfo.getShortName(); + Boolean isImapFavourite = (Boolean) prefs.get(key); + if (isImapFavourite == null || !isImapFavourite) + { + nonFavSites.add(siteInfo); + } + } + + return nonFavSites; + } + + /** + * Returns the text representing email body for ContentModel node. + * + * @param nodeRef NodeRef of the target content. + * @param type The type of the returned body. May be the one of {@link EmailBodyType}. + * @return Text representing email body for ContentModel node. + */ + public String getEmailBodyText(NodeRef nodeRef, EmailBodyType type) + { + return templateService.processTemplate(getDefaultEmailBodyTemplate(type), createEmailTemplateModel(nodeRef)); + } + + /** + * Returns default email body template. This method trying to find a template on the path in the repository first e.g. {@code "Data Dictionary > IMAP Templates >"}. This path + * should be set as the property of the "imapHelper" bean. In this case it returns {@code NodeRef.toString()} of the template. If there are no template in the repository it + * returns a default template on the classpath. + * + * @param type One of the {@link EmailBodyType}. + * @return String representing template classpath path or NodeRef.toString(). + */ + public String getDefaultEmailBodyTemplate(EmailBodyType type) + { + String result = null; + switch (type) + { + case TEXT_HTML: + result = CLASSPATH_TEXT_HTML_TEMPLATE; + break; + case TEXT_PLAIN: + result = CLASSPATH_TEXT_PLAIN_TEMPLATE; + break; + } + final StringBuilder templateName = new StringBuilder(DICTIONARY_TEMPLATE_PREFIX).append("-").append(type.getTypeSubtype()).append(".ftl"); + int indexOfStoreDelim = repositoryTemplatePath.indexOf(StoreRef.URI_FILLER); + if (indexOfStoreDelim == -1) + { + logger.error("Bad path format, " + StoreRef.URI_FILLER + " not found"); + return result; + } + indexOfStoreDelim += StoreRef.URI_FILLER.length(); + int indexOfPathDelim = repositoryTemplatePath.indexOf("/", indexOfStoreDelim); + if (indexOfPathDelim == -1) + { + logger.error("Bad path format, / not found"); + return result; + } + final String storePath = repositoryTemplatePath.substring(0, indexOfPathDelim); + final String rootPathInStore = repositoryTemplatePath.substring(indexOfPathDelim); + final String query = String.format("+PATH:\"%1$s/*\" +@cm\\:name:\"%2$s\"", rootPathInStore, templateName.toString()); + if (logger.isDebugEnabled()) + { + logger.debug("Using template path :" + repositoryTemplatePath + "/" + templateName); + logger.debug("Query: " + query); + } + StoreRef storeRef = new StoreRef(storePath); + ResultSet resultSet = searchService.query(storeRef, "lucene", query); + if (resultSet == null || resultSet.length() == 0) + { + logger.error(String.format("IMAP message template '%1$s' does not exist in the path '%2$s'.", templateName, repositoryTemplatePath)); + return result; + } + result = resultSet.getNodeRef(0).toString(); + return result; + } + + /** + * Builds default email template model for TemplateProcessor + * + * @param ref NodeRef of the target content. + * @return Map that includes template model objects. + */ + private Map createEmailTemplateModel(NodeRef ref) + { + Map model = new HashMap(8, 1.0f); + TemplateNode tn = new TemplateNode(ref, serviceRegistry, null); + model.put("document", tn); + NodeRef parent = nodeService.getPrimaryParent(ref).getParentRef(); + model.put("space", new TemplateNode(parent, serviceRegistry, null)); + model.put("date", new Date()); + model.put("contextUrl", new String(getWebApplicationContextUrl())); + model.put("alfTicket", new String(serviceRegistry.getAuthenticationService().getCurrentTicket())); + return model; + } + + private void checkForFlaggableAspect(NodeRef nodeRef) + { + if (!nodeService.hasAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE)) + { + Map aspectProperties = new HashMap(); + nodeService.addAspect(nodeRef, ImapModel.ASPECT_FLAGGABLE, aspectProperties); + } + } + + public boolean isPatchApplied() + { + return patchApplied; + } + + // ----------------------Getters and Setters---------------------------- + + public NodeService getNodeService() + { + return nodeService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public SearchService getSearchService() + { + return searchService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public FileFolderService getFileFolderService() + { + return fileFolderService; + } + + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + public TemplateService getTemplateService() + { + return templateService; + } + + public void setTemplateService(TemplateService templateService) + { + this.templateService = templateService; + } + + public NamespaceService getNamespaceService() + { + return namespaceService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public PermissionService getPermissionService() + { + return permissionService; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public ConfigService getConfigService() + { + return configService; + } + + public void setConfigService(ConfigService configService) + { + this.configService = configService; + } + + public DictionaryService getDictionaryService() + { + return dictionaryService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public PreferenceService getPreferenceService() + { + return preferenceService; + } + + public void setPreferenceService(PreferenceService preferenceService) + { + this.preferenceService = preferenceService; + } + + public SiteService getSiteService() + { + return siteService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public ServiceRegistry getServiceRegistry() + { + return serviceRegistry; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public PatchService getPatchService() + { + return patchService; + } + + public void setPatchService(PatchService patchService) + { + this.patchService = patchService; + } + +} diff --git a/source/java/org/alfresco/repo/imap/config/ImapConfigElement.java b/source/java/org/alfresco/repo/imap/config/ImapConfigElement.java new file mode 100755 index 0000000000..5d7762c7d7 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/config/ImapConfigElement.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap.config; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.alfresco.config.ConfigElement; +import org.alfresco.config.element.ConfigElementAdapter; + +public class ImapConfigElement extends ConfigElementAdapter +{ + private static final long serialVersionUID = -6911139959296875159L; + + public static final String CONFIG_ELEMENT_ID = "imapConfig"; + + private Map imapConfigs = new LinkedHashMap(8, 10f); + + public ImapConfigElement() + { + super(CONFIG_ELEMENT_ID); + } + + public ImapConfigElement(String name) + { + super(name); + } + + @Override + public ConfigElement combine(ConfigElement configElement) + { + ImapConfigElement combined = new ImapConfigElement(); + + // add all the imapConfigs from this element + for (ImapConfig imapConfig : getImapConfigs().values()) + { + combined.addImapConfig(imapConfig); + } + + // add all the imapConfigs from the given element + for (ImapConfig imapConfig : ((ImapConfigElement) configElement).getImapConfigs().values()) + { + combined.addImapConfig(imapConfig); + } + + return combined; + } + + public Map getImapConfigs() + { + return imapConfigs; + } + + public ImapConfig getImapConfig(String name) + { + return imapConfigs.get(name); + } + + void addImapConfig(ImapConfig imapConfig) + { + imapConfigs.put(imapConfig.getName(), imapConfig); + } + + public static class ImapConfig implements Serializable + { + private static final long serialVersionUID = 424330549937129149L; + + private String name; + private String mode; + private String store; + private String rootPath; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getMode() + { + return mode; + } + + public void setMode(String mode) + { + this.mode = mode; + } + + public String getStore() + { + return store; + } + + public void setStore(String store) + { + this.store = store; + } + + public String getRootPath() + { + return rootPath; + } + + public void setRootPath(String rootPath) + { + this.rootPath = rootPath; + } + + public static long getSerialVersionUID() + { + return serialVersionUID; + } + + } + +} diff --git a/source/java/org/alfresco/repo/imap/config/ImapElementReader.java b/source/java/org/alfresco/repo/imap/config/ImapElementReader.java new file mode 100755 index 0000000000..2e0e64b302 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/config/ImapElementReader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap.config; + +import java.util.Iterator; + +import org.alfresco.config.ConfigElement; +import org.alfresco.config.ConfigException; +import org.alfresco.config.xml.elementreader.ConfigElementReader; +import org.alfresco.repo.imap.config.ImapConfigElement.ImapConfig; +import org.dom4j.Element; + +public class ImapElementReader implements ConfigElementReader +{ + + private static final String ELEMENT_IMAP_CONFIG = "imapConfig"; + private static final String ELEMENT_IMAP = "imap"; + private static final String ELEMENT_STORE = "store"; + private static final String ELEMENT_ROOTPATH = "rootPath"; + + private static final String ATTR_NAME = "name"; + private static final String ATTR_MODE = "mode"; + + public ConfigElement parse(Element element) + { + ImapConfigElement configElement = null; + + if (element != null) + { + String elementName = element.getName(); + if (elementName.equals(ELEMENT_IMAP_CONFIG) == false) + { + throw new ConfigException("ImapElementReader can parse '" + ELEMENT_IMAP_CONFIG + "' elements only, the element passed is '" + elementName + "'"); + } + + configElement = new ImapConfigElement(); + + for (Iterator items = element.elementIterator(ELEMENT_IMAP); items.hasNext();) + { + Element item = items.next(); + + String name = item.attributeValue(ATTR_NAME); + String mode = item.attributeValue(ATTR_MODE); + String store = item.element(ELEMENT_STORE).getStringValue(); + String rootPath = item.element(ELEMENT_ROOTPATH).getStringValue(); + + ImapConfig imapConfig = new ImapConfig(); + imapConfig.setName(name); + imapConfig.setMode(mode); + imapConfig.setStore(store); + imapConfig.setRootPath(rootPath); + + configElement.addImapConfig(imapConfig); + } + } + return configElement; + } + +} diff --git a/source/java/org/alfresco/repo/imap/exception/AlfrescoImapFolderException.java b/source/java/org/alfresco/repo/imap/exception/AlfrescoImapFolderException.java new file mode 100755 index 0000000000..54bf098ed3 --- /dev/null +++ b/source/java/org/alfresco/repo/imap/exception/AlfrescoImapFolderException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.imap.exception; + +import com.icegreen.greenmail.store.FolderException; + +/** + * Thrown on an inappropriate attempt to modify a folder. + * + * @author Ivan Rybnikov + */ +public class AlfrescoImapFolderException extends FolderException +{ + + private static final long serialVersionUID = -2721708848846740336L; + + public final static String PERMISSION_DENIED = "Can't create folder - Permission denied"; + + public AlfrescoImapFolderException(String message) + { + super(message); + } + +} diff --git a/source/java/org/alfresco/repo/jscript/Search.java b/source/java/org/alfresco/repo/jscript/Search.java index 5f22d3e39a..8af5008ae5 100644 --- a/source/java/org/alfresco/repo/jscript/Search.java +++ b/source/java/org/alfresco/repo/jscript/Search.java @@ -48,6 +48,8 @@ import org.dom4j.io.SAXReader; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; +import com.werken.saxpath.XPathReader; + /** * Search component for use by the ScriptService. *

@@ -215,6 +217,26 @@ public final class Search extends BaseScopableProcessorExtension } } + /** + * Validation Xpath query + * + * @param query xpath query + * @return true if xpath query valid + */ + public boolean isValidXpathQuery(String query) + { + try + { + XPathReader reader = new XPathReader(); + reader.parse(query); + } + catch (Exception e) + { + return false; + } + return true; + } + /** * Execute a Lucene search *