001/*
002 * (C) Copyright 2006-2019 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 */
019
020package org.nuxeo.ecm.platform.mail.utils;
021
022import static java.lang.Boolean.FALSE;
023import static java.lang.Boolean.TRUE;
024import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.CORE_SESSION_KEY;
025import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.EMAILS_LIMIT_PROPERTY_NAME;
026import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.EMAIL_PROPERTY_NAME;
027import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.HOST_PROPERTY_NAME;
028import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.IMAP;
029import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.IMAPS;
030import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.LEAVE_ON_SERVER_KEY;
031import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.MIMETYPE_SERVICE_KEY;
032import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.PARENT_PATH_KEY;
033import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.PASSWORD_PROPERTY_NAME;
034import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.POP3S;
035import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.PORT_PROPERTY_NAME;
036import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.PROTOCOL_TYPE_KEY;
037import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.PROTOCOL_TYPE_PROPERTY_NAME;
038import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SOCKET_FACTORY_FALLBACK_PROPERTY_NAME;
039import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SOCKET_FACTORY_PORT_PROPERTY_NAME;
040import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.SSL_PROTOCOLS_PROPERTY_NAME;
041import static org.nuxeo.ecm.platform.mail.utils.MailCoreConstants.STARTTLS_ENABLE_PROPERTY_NAME;
042import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_STORE_PROTOCOL;
043
044import java.util.ArrayList;
045import java.util.List;
046import java.util.Properties;
047import java.util.concurrent.CopyOnWriteArrayList;
048
049import javax.mail.FetchProfile;
050import javax.mail.Flags;
051import javax.mail.Flags.Flag;
052import javax.mail.Folder;
053import javax.mail.Message;
054import javax.mail.MessagingException;
055import javax.mail.Store;
056
057import org.apache.commons.lang3.StringUtils;
058import org.apache.commons.logging.Log;
059import org.apache.commons.logging.LogFactory;
060import org.nuxeo.ecm.core.api.CoreSession;
061import org.nuxeo.ecm.core.api.DocumentModel;
062import org.nuxeo.ecm.core.api.trash.TrashService;
063import org.nuxeo.ecm.platform.mail.action.ExecutionContext;
064import org.nuxeo.ecm.platform.mail.action.MessageActionPipe;
065import org.nuxeo.ecm.platform.mail.action.Visitor;
066import org.nuxeo.ecm.platform.mail.listener.MailEventListener;
067import org.nuxeo.ecm.platform.mail.service.MailService;
068import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
069import org.nuxeo.mail.MailSessionBuilder;
070import org.nuxeo.runtime.api.Framework;
071
072/**
073 * Helper for Mail Core.
074 *
075 * @author Catalin Baican
076 */
077public final class MailCoreHelper {
078
079    private static final Log log = LogFactory.getLog(MailEventListener.class);
080
081    public static final String PIPE_NAME = "nxmail";
082
083    public static final String INBOX = "INBOX";
084
085    /**
086     * @deprecated since 10.3, use {@link TrashService} instead
087     */
088    @Deprecated
089    public static final String DELETED_LIFECYCLE_STATE = "deleted";
090
091    public static final long EMAILS_LIMIT_DEFAULT = 100;
092
093    public static final String IMAP_DEBUG = "org.nuxeo.mail.imap.debug";
094
095    protected static final CopyOnWriteArrayList<String> processingMailBoxes = new CopyOnWriteArrayList<>();
096
097    private MailCoreHelper() {
098    }
099
100    /**
101     * Creates MailMessage documents for every unread mail found in the INBOX. The parameters needed to connect to the
102     * email INBOX are retrieved from the MailFolder document passed as a parameter.
103     */
104    public static void checkMail(DocumentModel currentMailFolder, CoreSession coreSession) throws MessagingException {
105
106        if (processingMailBoxes.addIfAbsent(currentMailFolder.getId())) {
107            try {
108                doCheckMail(currentMailFolder, coreSession);
109            } finally {
110                processingMailBoxes.remove(currentMailFolder.getId());
111            }
112        } else {
113            log.info("Mailbox " + currentMailFolder.getPathAsString() + " is already being processed");
114        }
115    }
116
117    protected static void doCheckMail(DocumentModel currentMailFolder, CoreSession coreSession)
118            throws MessagingException {
119        String email = (String) currentMailFolder.getPropertyValue(EMAIL_PROPERTY_NAME);
120        String password = (String) currentMailFolder.getPropertyValue(PASSWORD_PROPERTY_NAME);
121        if (!StringUtils.isEmpty(email) && !StringUtils.isEmpty(password)) {
122            MailService mailService = Framework.getService(MailService.class);
123
124            MessageActionPipe pipe = mailService.getPipe(PIPE_NAME);
125
126            Visitor visitor = new Visitor(pipe);
127            Thread.currentThread().setContextClassLoader(Framework.class.getClassLoader());
128
129            // initialize context
130            ExecutionContext initialExecutionContext = new ExecutionContext();
131
132            initialExecutionContext.put(MIMETYPE_SERVICE_KEY, Framework.getService(MimetypeRegistry.class));
133
134            initialExecutionContext.put(PARENT_PATH_KEY, currentMailFolder.getPathAsString());
135
136            initialExecutionContext.put(CORE_SESSION_KEY, coreSession);
137
138            // TODO should be an attribute in 'protocol' schema
139            initialExecutionContext.put(LEAVE_ON_SERVER_KEY, TRUE);
140
141            Folder rootFolder = null;
142            Store store = null;
143            try {
144                String protocolType = (String) currentMailFolder.getPropertyValue(PROTOCOL_TYPE_PROPERTY_NAME);
145                initialExecutionContext.put(PROTOCOL_TYPE_KEY, protocolType);
146
147                String host = (String) currentMailFolder.getPropertyValue(HOST_PROPERTY_NAME);
148                String port = (String) currentMailFolder.getPropertyValue(PORT_PROPERTY_NAME);
149                Boolean socketFactoryFallback = (Boolean) currentMailFolder.getPropertyValue(
150                        SOCKET_FACTORY_FALLBACK_PROPERTY_NAME);
151                String socketFactoryPort = (String) currentMailFolder.getPropertyValue(
152                        SOCKET_FACTORY_PORT_PROPERTY_NAME);
153                Boolean starttlsEnable = (Boolean) currentMailFolder.getPropertyValue(STARTTLS_ENABLE_PROPERTY_NAME);
154                String sslProtocols = (String) currentMailFolder.getPropertyValue(SSL_PROTOCOLS_PROPERTY_NAME);
155                Long emailsLimit = (Long) currentMailFolder.getPropertyValue(EMAILS_LIMIT_PROPERTY_NAME);
156                long emailsLimitLongValue = emailsLimit == null ? EMAILS_LIMIT_DEFAULT : emailsLimit.longValue();
157
158                String imapDebug = Framework.getProperty(IMAP_DEBUG, FALSE.toString());
159
160                Properties properties = new Properties();
161                properties.put(CONFIGURATION_MAIL_STORE_PROTOCOL, protocolType);
162                // Is IMAP connection
163                if (IMAP.equals(protocolType)) {
164                    properties.put("mail.imap.host", host);
165                    properties.put("mail.imap.port", port);
166                    properties.put("mail.imap.starttls.enable", starttlsEnable.toString());
167                    properties.put("mail.imap.debug", imapDebug);
168                    properties.put("mail.imap.partialfetch", FALSE.toString());
169                } else if (IMAPS.equals(protocolType)) {
170                    properties.put("mail.imaps.host", host);
171                    properties.put("mail.imaps.port", port);
172                    properties.put("mail.imaps.starttls.enable", starttlsEnable.toString());
173                    properties.put("mail.imaps.ssl.protocols", sslProtocols);
174                    properties.put("mail.imap.partialfetch", FALSE.toString());
175                    properties.put("mail.imaps.partialfetch", FALSE.toString());
176                } else if (POP3S.equals(protocolType)) {
177                    properties.put("mail.pop3s.host", host);
178                    properties.put("mail.pop3s.port", port);
179                    properties.put("mail.pop3s.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
180                    properties.put("mail.pop3s.socketFactory.fallback", socketFactoryFallback.toString());
181                    properties.put("mail.pop3s.socketFactory.port", socketFactoryPort);
182                    properties.put("mail.pop3s.ssl.protocols", sslProtocols);
183                } else {
184                    // Is POP3 connection
185                    properties.put("mail.pop3.host", host);
186                    properties.put("mail.pop3.port", port);
187                }
188
189                properties.put("user", email);
190                properties.put("password", password);
191
192                store = MailSessionBuilder.fromProperties(properties).buildAndConnect();
193
194                String folderName = INBOX; // TODO should be an attribute in 'protocol' schema
195                rootFolder = store.getFolder(folderName);
196
197                // need RW access to update message flags
198                rootFolder.open(Folder.READ_WRITE);
199
200                Message[] allMessages = rootFolder.getMessages();
201                // VDU
202                log.debug("nbr of messages in folder:" + allMessages.length);
203
204                FetchProfile fetchProfile = new FetchProfile();
205                fetchProfile.add(FetchProfile.Item.FLAGS);
206                fetchProfile.add(FetchProfile.Item.ENVELOPE);
207                fetchProfile.add(FetchProfile.Item.CONTENT_INFO);
208                fetchProfile.add("Message-ID");
209                fetchProfile.add("Content-Transfer-Encoding");
210
211                rootFolder.fetch(allMessages, fetchProfile);
212
213                List<Message> unreadMessagesList = new ArrayList<>();
214                for (Message message : allMessages) {
215                    Flags flags = message.getFlags();
216                    int unreadMessagesListSize = unreadMessagesList.size();
217                    if (flags != null && !flags.contains(Flag.SEEN) && unreadMessagesListSize < emailsLimitLongValue) {
218                        unreadMessagesList.add(message);
219                        if (unreadMessagesListSize == emailsLimitLongValue - 1) {
220                            break;
221                        }
222                    }
223                }
224
225                Message[] unreadMessagesArray = unreadMessagesList.toArray(new Message[0]);
226
227                // perform email import
228                visitor.visit(unreadMessagesArray, initialExecutionContext);
229
230                // perform flag update globally
231                Flags flags = new Flags();
232                flags.add(Flag.SEEN);
233
234                boolean leaveOnServer = (Boolean) initialExecutionContext.get(LEAVE_ON_SERVER_KEY);
235                if ((IMAP.equals(protocolType) || IMAPS.equals(protocolType)) && leaveOnServer) {
236                    flags.add(Flag.SEEN);
237                } else {
238                    flags.add(Flag.DELETED);
239                }
240                rootFolder.setFlags(unreadMessagesArray, flags, true);
241
242            } finally {
243                if (rootFolder != null && rootFolder.isOpen()) {
244                    rootFolder.close(true);
245                }
246                if (store != null) {
247                    store.close();
248                }
249            }
250        }
251    }
252
253}