/* * Copyright (C) 2005-2011 Alfresco Software Limited. * * This file is part of Alfresco * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . */ package org.alfresco.repo.imap; import java.io.IOException; 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.NavigableMap; import javax.mail.Flags; import javax.mail.Flags.Flag; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import org.alfresco.model.ContentModel; import org.alfresco.model.ImapModel; import org.alfresco.repo.imap.AlfrescoImapConst.ImapViewMode; import org.alfresco.repo.imap.ImapService.FolderStatus; import org.alfresco.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.security.AccessStatus; import org.alfresco.util.GUID; import org.alfresco.util.Utf7; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.icegreen.greenmail.foedus.util.MsgRangeFilter; 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; /** * Implementation of greenmail MailFolder. It represents an Alfresco content folder and handles * appendMessage, copyMessage, expunge (delete), getMessages, getMessage and so requests. * * @author Mike Shavnev * @author David Ward */ public class AlfrescoImapFolder extends AbstractImapFolder implements Serializable { private static final long serialVersionUID = -7223111284066976111L; private static Log logger = LogFactory.getLog(AlfrescoImapFolder.class); /** * Reference to the {@link FileInfo} object representing the folder. */ private final FileInfo folderInfo; /** * Name of the folder. */ private final String folderName; private final String folderPath; private final String userName; private final int mountPointId; /** * Defines view mode. */ private final ImapViewMode viewMode; /** * Reference to the {@link ImapService} object. */ private final ImapService imapService; /** * Defines whether the folder is selectable or not. */ private final boolean selectable; private final boolean extractAttachmentsEnabled; /** * Cached Folder status information, validated against a change token. */ private FolderStatus folderStatus; private static final Flags PERMANENT_FLAGS = new Flags(); 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); } public boolean isExtractAttachmentsEnabled() { return extractAttachmentsEnabled; } /** * Protected constructor for the hierarchy delimiter */ AlfrescoImapFolder(String userName, ImapService imapService, ServiceRegistry serviceRegistry) { this(null, userName, "", "", null, imapService, serviceRegistry, false, false, 0); } /** * Constructs {@link AlfrescoImapFolder} 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. */ public AlfrescoImapFolder( FileInfo folderInfo, String userName, String folderName, String folderPath, ImapViewMode viewMode, boolean extractAttachmentsEnabled, ImapService imapService, ServiceRegistry serviceRegistry, int mountPointId) { this(folderInfo, userName, folderName, folderPath, viewMode, imapService, serviceRegistry, null, extractAttachmentsEnabled, mountPointId); } /** * Constructs {@link AlfrescoImapFolder} 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 imapService - the IMAP service. * @param selectable - defines whether the folder is selectable or not. */ public AlfrescoImapFolder( FileInfo folderInfo, String userName, String folderName, String folderPath, ImapViewMode viewMode, ImapService imapService, ServiceRegistry serviceRegistry, Boolean selectable, boolean extractAttachmentsEnabled, int mountPointId) { super(serviceRegistry); this.folderInfo = folderInfo; this.userName = userName; this.folderName = folderName != null ? folderName : (folderInfo != null ? folderInfo.getName() : null); this.folderPath = folderPath; this.viewMode = viewMode != null ? viewMode : ImapViewMode.ARCHIVE; this.extractAttachmentsEnabled = extractAttachmentsEnabled; this.imapService = imapService; // 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 = !serviceRegistry.getNodeService().hasAspect(folderInfo.getNodeRef(), ImapModel.ASPECT_IMAP_FOLDER_NONSELECTABLE); if (storedSelectable == null) { this.selectable = true; } else { this.selectable = storedSelectable; } } else { this.selectable = selectable; } } else { this.selectable = false; } this.mountPointId = mountPointId; } /* * (non-Javadoc) * @see com.icegreen.greenmail.store.MailFolder#getFullName() */ @Override public String getFullName() { return Utf7.encode(AlfrescoImapConst.USER_NAMESPACE + AlfrescoImapConst.HIERARCHY_DELIMITER + this.userName + AlfrescoImapConst.HIERARCHY_DELIMITER + getFolderPath(), Utf7.UTF7_MODIFIED); } /* (non-Javadoc) * @see com.icegreen.greenmail.store.MailFolder#getName() */ @Override public String getName() { return this.folderName; } /* (non-Javadoc) * @see com.icegreen.greenmail.store.MailFolder#isSelectable() */ @Override public boolean isSelectable() { return this.selectable; } /** * Returns the contents of this folder. * * @return A sorted map of UIDs to FileInfo objects. */ private NavigableMap searchMails() { return getFolderStatus().search; } /** * Invalidates the current cached state * * @return true if this instance is still valid for reuse */ public boolean reset() { this.folderStatus = null; return new CommandCallback() { public Boolean command() throws Throwable { return serviceRegistry.getNodeService().exists(folderInfo.getNodeRef()); } }.run(true); } protected FolderStatus getFolderStatus() { if (this.folderStatus == null) { CommandCallback command = new CommandCallback() { public FolderStatus command() throws Throwable { return imapService.getFolderStatus(userName, folderInfo.getNodeRef(), viewMode); } }; this.folderStatus = command.run(); } return this.folderStatus; } /** * Appends message to the folder. * * @param message - message. * @param flags - message flags. * @param internalDate - not used. Current date used instead. */ @Override protected long appendMessageInternal( MimeMessage message, Flags flags, Date internalDate) throws FileExistsException, FileNotFoundException, IOException, MessagingException { long uid = createMimeMessageInFolder(this.folderInfo, message, flags); // Invalidate current folder status this.folderStatus = null; return uid; } /** * Copies message with the given UID to the specified {@link MailFolder}. * * @param uid - UID of the message * @param toFolder - reference to the destination folder. * @throws MessagingException * @throws IOException * @throws FileNotFoundException * @throws FileExistsException */ @Override protected void copyMessageInternal( long uid, MailFolder toFolder) throws MessagingException, FileExistsException, FileNotFoundException, IOException { AlfrescoImapFolder toImapMailFolder = (AlfrescoImapFolder) toFolder; NodeRef destFolderNodeRef = toImapMailFolder.getFolderInfo().getNodeRef(); FileInfo sourceMessageFileInfo = searchMails().get(uid); if (serviceRegistry.getNodeService().hasAspect(sourceMessageFileInfo.getNodeRef(), ImapModel.ASPECT_IMAP_CONTENT)) { //Generate body of message MimeMessage newMessage = new ImapModelMessage(sourceMessageFileInfo, serviceRegistry, true); toImapMailFolder.appendMessageInternal(newMessage, imapService.getFlags(sourceMessageFileInfo), new Date()); } else { String fileName = (String) serviceRegistry.getNodeService().getProperty(sourceMessageFileInfo.getNodeRef(), ContentModel.PROP_NAME); String newFileName = imapService.generateUniqueFilename(destFolderNodeRef, fileName); serviceRegistry.getFileFolderService().copy(sourceMessageFileInfo.getNodeRef(), destFolderNodeRef, newFileName); } } /** * Marks all messages in the folder as deleted using {@link Flags.Flag#DELETED} flag. */ @Override public void deleteAllMessagesInternal() throws FolderException { if (isReadOnly()) { throw new FolderException("Can't delete all - Permission denied"); } for (Map.Entry entry : searchMails().entrySet()) { imapService.setFlag(entry.getValue(), Flags.Flag.DELETED, true); // comment out to physically remove content. // fileFolderService.delete(fileInfo.getNodeRef()); } } /** * Deletes messages marked with {@link Flags.Flag#DELETED}. Note that this message deletes all messages with this flag. */ @Override protected void expungeInternal() throws FolderException { if (isReadOnly()) { throw new FolderException("Can't expunge - Permission denied"); } for (Map.Entry entry : searchMails().entrySet()) { imapService.expungeMessage(entry.getValue()); } } /** * Returns the MSN number of the first unseen message. * * @return MSN number of the first unseen message. */ @Override public int getFirstUnseen() { return getFolderStatus().firstUnseen; } /** * Returns message by its UID. * * @param uid - UID of the message. * @return message. * @throws MessagingException */ @Override protected SimpleStoredMessage getMessageInternal(long uid) throws MessagingException { if (logger.isDebugEnabled()) { logger.debug("[getMessageInternal] " + this); } FileInfo mesInfo = searchMails().get(uid); if (mesInfo == null) { return null; } return imapService.getMessage(mesInfo); } /** * Returns count of the messages in the folder. * * @return Count of the messages. */ @Override public int getMessageCount() { return getFolderStatus().messageCount; } /** * Returns UIDs of all messages in the folder. * * @return UIDS of the messages. */ @Override public long[] getMessageUids() { if (logger.isDebugEnabled()) { logger.debug("[getMessageUidsInternal] " + this); } Collection uidSet = searchMails().keySet(); long[] uids = new long[uidSet.size()]; int i = 0; for (Long uid : uidSet) { uids[i++] = uid; } return uids; } /** * Returns list of all messages in the folder. * * @return list of {@link SimpleStoredMessage} objects. */ @Override protected List getMessagesInternal() { if (logger.isDebugEnabled()) { logger.debug("[getMessagesInternal] " + this); } return convertToMessages(searchMails().values()); } private List convertToMessages(Collection fileInfos) { if (logger.isDebugEnabled()) { logger.debug("[convertToMessages] " + this); } if (fileInfos == null || fileInfos.size() == 0) { logger.debug("[convertToMessages] - fileInfos is empty or null"); return Collections.emptyList(); } List result = new LinkedList(); for (FileInfo fileInfo : fileInfos) { try { result.add(imapService.createImapMessage(fileInfo, false)); if (logger.isDebugEnabled()) { logger.debug("[convertToMessages] Message added: " + fileInfo.getName()); } } catch (MessagingException e) { logger.warn("[convertToMessages] Invalid message! File name:" + fileInfo.getName(), e); } } return result; } /** * Returns list of messages by filter. * * @param msgRangeFilter - {@link MsgRangeFilter} object representing filter. * @return list of filtered messages. */ @Override protected List getMessagesInternal(MsgRangeFilter msgRangeFilter) { throw new UnsupportedOperationException("IMAP implementation doesn't support POP3 requests"); } /** * 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. */ @Override public int getMsn(long uid) throws FolderException { NavigableMap messages = searchMails(); if (!messages.containsKey(uid)) { throw new FolderException("No such message."); } return messages.headMap(uid, true).size(); } /** * Returns the list of messages that have no {@link Flags.Flag#DELETED} flag set for current user. * * @return the list of non-deleted messages. */ @Override protected List getNonDeletedMessagesInternal() { if (logger.isDebugEnabled()) { logger.debug("[getNonDeletedMessagesInternal] " + this); } List result = new ArrayList(); Collection values = getMessagesInternal(); for (SimpleStoredMessage message : values) { if (!getFlags(message).contains(Flags.Flag.DELETED)) { result.add(message); } } if (logger.isDebugEnabled() && folderInfo != null) { logger.debug(folderInfo.getName() + " - Non deleted messages count:" + result.size()); } return result; } /** * Returns permanent flags. * * @return {@link Flags} object containing flags. */ @Override 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. */ @Override public int getRecentCount(boolean reset) { int recent = getFolderStatus().recentCount; if (reset && recent > 0) { CommandCallback command = new CommandCallback() { public Void command() throws Throwable { for (FileInfo fileInfo : folderStatus.search.values()) { Flags flags = imapService.getFlags(fileInfo); if (flags.contains(Flags.Flag.RECENT)) { imapService.setFlag(fileInfo, Flags.Flag.RECENT, false); } } return null; } }; command.run(); } return recent; } /** * Returns UIDNEXT value of the folder. * * @return UIDNEXT value. */ @Override public long getUidNext() { NavigableMap search = getFolderStatus().search; return search.isEmpty() ? 1 : search.lastKey() + 1; } /** * Returns UIDVALIDITY value of the folder. * * @return UIDVALIDITY value. */ @Override public long getUidValidity() { return getFolderStatus().uidValidity + mountPointId; } /** * 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. */ @Override public int getUnseenCount() { return getFolderStatus().unseenCount; } /** * 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. * @throws FolderException * @throws MessagingException */ @Override protected void replaceFlagsInternal( Flags flags, long uid, FolderListener silentListener, boolean addUid) throws FolderException, MessagingException { int msn = getMsn(uid); FileInfo fileInfo = searchMails().get(uid); imapService.setFlags(fileInfo, MessageFlags.ALL_FLAGS, false); imapService.setFlags(fileInfo, flags, true); Long uidNotification = addUid ? uid : null; notifyFlagUpdate(msn, flags, uidNotification, silentListener); } /** * 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. * @throws MessagingException * @throws FolderException */ @Override protected void setFlagsInternal( Flags flags, boolean value, long uid, FolderListener silentListener, boolean addUid) throws MessagingException, FolderException { int msn = getMsn(uid); FileInfo fileInfo = searchMails().get(uid); imapService.setFlags(fileInfo, flags, value); Long uidNotification = null; if (addUid) { uidNotification = new Long(uid); } notifyFlagUpdate(msn, flags, uidNotification, silentListener); } private Flags getFlags(SimpleStoredMessage mess) { return ((AbstractMimeMessage) mess.getMimeMessage()).getFlags(); } // ----------------------Getters and Setters---------------------------- public String getFolderPath() { return this.folderPath; } public FileInfo getFolderInfo() { return folderInfo; } /* (non-Javadoc) * @see org.alfresco.repo.imap.AbstractImapFolder#isMarkedInternal() */ @Override public boolean isMarked() { FolderStatus folderStatus = getFolderStatus(); return folderStatus.recentCount > 0 || folderStatus.unseenCount > 0; } /** * Whether the folder is read-only for user. * * @return {@code boolean} */ @Override protected boolean isReadOnly() { AccessStatus status = serviceRegistry.getPublicServiceAccessService().hasAccess(ServiceRegistry.NODE_SERVICE.getLocalName(), "createNode", folderInfo.getNodeRef(), null, null, null); //serviceRegistry.getPermissionService().hasPermission(folderInfo.getNodeRef(), PermissionService.WRITE); return status == AccessStatus.DENIED; } public ImapViewMode getViewMode() { return viewMode; } /** * Creates the EML message in the specified folder. * * @param folderFileInfo The folder to create message in. * @param message The original MimeMessage. * @return ID of the new message created * @throws FileNotFoundException * @throws FileExistsException * @throws MessagingException * @throws IOException */ private long createMimeMessageInFolder( FileInfo folderFileInfo, MimeMessage message, Flags flags) throws FileExistsException, FileNotFoundException, IOException, MessagingException { String name = AlfrescoImapConst.MESSAGE_PREFIX + GUID.generate(); FileFolderService fileFolderService = serviceRegistry.getFileFolderService(); FileInfo messageFile = fileFolderService.create(folderFileInfo.getNodeRef(), name, ContentModel.TYPE_CONTENT); final long newMessageUid = (Long) messageFile.getProperties().get(ContentModel.PROP_NODE_DBID); name = AlfrescoImapConst.MESSAGE_PREFIX + newMessageUid + AlfrescoImapConst.EML_EXTENSION; fileFolderService.rename(messageFile.getNodeRef(), name); Flags newFlags = new Flags(flags); newFlags.add(Flag.RECENT); imapService.setFlags(messageFile, newFlags, true); if (extractAttachmentsEnabled) { imapService.extractAttachments(messageFile.getNodeRef(), message); } // Force persistence of the message to the repository new IncomingImapMessage(messageFile, serviceRegistry, message); return newMessageUid; } }