001/*
002 * (C) Copyright 2006-2018 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 *     Alexandre Russel
018 *
019 */
020
021package org.nuxeo.ecm.platform.mail.service;
022
023import java.util.HashMap;
024import java.util.Map;
025import java.util.Properties;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.stream.Collectors;
028
029import javax.mail.Address;
030import javax.mail.Folder;
031import javax.mail.Message;
032import javax.mail.MessagingException;
033import javax.mail.Session;
034import javax.mail.Store;
035import javax.mail.Transport;
036import javax.mail.internet.MimeMessage;
037
038import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper;
039import org.nuxeo.ecm.platform.mail.action.MailBoxActions;
040import org.nuxeo.ecm.platform.mail.action.MailBoxActionsImpl;
041import org.nuxeo.ecm.platform.mail.action.MessageActionPipe;
042import org.nuxeo.ecm.platform.mail.action.MessageActionPipeDescriptor;
043import org.nuxeo.ecm.platform.mail.fetcher.PropertiesFetcher;
044import org.nuxeo.ecm.platform.mail.fetcher.PropertiesFetcherDescriptor;
045import org.nuxeo.runtime.api.Framework;
046import org.nuxeo.runtime.model.ComponentContext;
047import org.nuxeo.runtime.model.ComponentInstance;
048import org.nuxeo.runtime.model.DefaultComponent;
049
050/**
051 * @author Alexandre Russel
052 */
053public class MailServiceImpl extends DefaultComponent implements MailService {
054
055    private static final String SESSION_FACTORY = "sessionFactory";
056
057    private static final String PROPERTIES_FETCHER = "propertiesFetcher";
058
059    private static final String ACTION_PIPES = "actionPipes";
060
061    /**
062     * Fetchers aggregated by name.
063     */
064    private final Map<String, Class<? extends PropertiesFetcher>> fetchers = new HashMap<>();
065
066    /**
067     * Session factories aggregated by name.
068     */
069    private final Map<String, SessionFactoryDescriptor> sessionFactories = new HashMap<>();
070
071    /**
072     * Fetchers aggregated by session factory name.
073     */
074    private final Map<String, PropertiesFetcher> configuredFetchers = new HashMap<>();
075
076    private final Map<String, MessageActionPipe> actionPipesRegistry = new HashMap<>();
077
078    private final Map<String, MessageActionPipeDescriptor> actionPipeDescriptorsRegistry = new HashMap<>();
079
080    protected final Map<String, Session> sessions = new ConcurrentHashMap<>();
081
082    static {
083        setDecodeUTFFileNamesSystemProperty();
084    }
085
086    @Override
087    public void stop(ComponentContext context) throws InterruptedException {
088        sessions.clear();
089    }
090
091    @Override
092    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
093        if (extensionPoint.equals(SESSION_FACTORY)) {
094            SessionFactoryDescriptor descriptor = (SessionFactoryDescriptor) contribution;
095            registerSessionFactory(descriptor);
096        } else if (extensionPoint.equals(PROPERTIES_FETCHER)) {
097            PropertiesFetcherDescriptor descriptor = (PropertiesFetcherDescriptor) contribution;
098            fetchers.put(descriptor.getName(), descriptor.getFetcher());
099        } else if (extensionPoint.equals(ACTION_PIPES)) {
100            MessageActionPipeDescriptor descriptor = (MessageActionPipeDescriptor) contribution;
101            registerActionPipe(descriptor);
102        }
103    }
104
105    @Override
106    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
107        // TODO deal with other extension points
108        if (extensionPoint.equals(ACTION_PIPES)) {
109            MessageActionPipeDescriptor descriptor = (MessageActionPipeDescriptor) contribution;
110            actionPipesRegistry.remove(descriptor.getName());
111        }
112    }
113
114    private void registerSessionFactory(SessionFactoryDescriptor descriptor) {
115        sessionFactories.put(descriptor.getName(), descriptor);
116    }
117
118    private void registerActionPipe(MessageActionPipeDescriptor descriptor) {
119        if (!descriptor.getOverride()) {
120            MessageActionPipeDescriptor existingDescriptor = actionPipeDescriptorsRegistry.get(descriptor.getName());
121            if (existingDescriptor != null) {
122                descriptor.merge(existingDescriptor);
123            }
124        }
125        actionPipeDescriptorsRegistry.put(descriptor.getName(), descriptor);
126        actionPipesRegistry.put(descriptor.getName(), descriptor.getPipe());
127    }
128
129    private static void setDecodeUTFFileNamesSystemProperty() {
130        String toDecodeTheFilenames = Framework.getRuntime().getProperty("mail.mime.decodefilename");
131        if (toDecodeTheFilenames == null) {
132            return;
133        }
134        toDecodeTheFilenames = toDecodeTheFilenames.trim().toLowerCase();
135        if (toDecodeTheFilenames.equals("true") || toDecodeTheFilenames.equals("yes")) {
136            System.setProperty("mail.mime.decodefilename", "true");
137            return;
138        }
139        System.setProperty("mail.mime.decodefilename", "false");
140    }
141
142    public Store getConnectedStore(String name) throws MessagingException {
143        return getConnectedStore(name, null);
144    }
145
146    public Store getConnectedStore(String name, Map<String, Object> context) throws MessagingException {
147        Properties props = getProperties(name, context);
148        Session session = newSession(props);
149        Store store = session.getStore();
150        store.connect(props.getProperty("user"), props.getProperty("password"));
151        return store;
152    }
153
154    private Properties getProperties(String name, Map<String, Object> map) {
155        return getFetcher(name).getProperties(map);
156    }
157
158    public Transport getConnectedTransport(String name) throws MessagingException {
159        return getConnectedTransport(name, null);
160    }
161
162    public Transport getConnectedTransport(String name, Map<String, Object> context) throws MessagingException {
163        Properties props = getProperties(name, context);
164        Session session = newSession(props);
165        Transport transport = session.getTransport();
166        transport.connect(props.getProperty("user"), props.getProperty("password"));
167        return transport;
168    }
169
170    public Session getSession(String name) {
171        return getSession(name, null);
172    }
173
174    public Session getSession(String name, Map<String, Object> context) {
175        Properties props = getProperties(name, context);
176        return newSession(props);
177    }
178
179    public MailBoxActions getMailBoxActions(String factoryName, String folderName) throws MessagingException {
180        return getMailBoxActions(factoryName, folderName, null);
181    }
182
183    public MailBoxActions getMailBoxActions(String factoryName, String folderName, Map<String, Object> context)
184            throws MessagingException {
185        Store store = getConnectedStore(factoryName, context);
186        Folder folder = store.getFolder(folderName);
187        return new MailBoxActionsImpl(folder, true);
188    }
189
190    public void sendMail(String text, String subject, String factory, Address[] recipients) {
191        sendMail(text, subject, factory, recipients, null);
192    }
193
194    public void sendMail(String text, String subject, String factory, Address[] recipients, Map<String, Object> context) {
195        Session session = getSession(factory, context);
196        Message message = new MimeMessage(session);
197        try {
198            message.setFrom();
199            message.setSubject(subject);
200            message.setRecipients(Message.RecipientType.TO, recipients);
201            message.setText(text);
202            Transport.send(message);
203        } catch (MessagingException e) {
204            throw new RuntimeException(e);
205        }
206    }
207
208    public PropertiesFetcher getFetcher(String name) {
209        PropertiesFetcher fetcher = configuredFetchers.get(name);
210        if (fetcher == null) {
211            String fetcherName = sessionFactories.get(name).getFetcherName();
212            Class<? extends PropertiesFetcher> clazz = fetchers.get(fetcherName);
213            SessionFactoryDescriptor descriptor = sessionFactories.get(name);
214            try {
215                fetcher = clazz.newInstance();
216            } catch (ReflectiveOperationException e) {
217                throw new RuntimeException(e);
218            }
219            fetcher.configureFetcher(descriptor.getProperties());
220            configuredFetchers.put(name, fetcher);
221        }
222        return fetcher;
223    }
224
225    public MessageActionPipe getPipe(String name) {
226        return actionPipesRegistry.get(name);
227    }
228
229    protected Session newSession(Properties props) {
230        // build a key for sessions cache
231        String sessionKey = props.entrySet()
232                                 .stream()
233                                 .map(e -> e.getKey() + "#" + e.getValue())
234                                 .sorted()
235                                 .collect(Collectors.joining("-", "{", "}"));
236        return sessions.computeIfAbsent(sessionKey, k -> EmailHelper.newSession(props));
237    }
238}