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}