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