001/*
002 * (C) Copyright 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 *      Kevin Leturc <kleturc@nuxeo.com>
018 */
019
020package org.nuxeo.mail;
021
022import static java.lang.Boolean.FALSE;
023import static java.util.function.Predicate.not;
024import static java.util.stream.Collectors.toMap;
025import static org.apache.commons.lang3.StringUtils.isNotEmpty;
026import static org.nuxeo.mail.MailConstants.CONFIGURATION_JNDI_JAVA_MAIL;
027import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_DEBUG;
028import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_PREFIX;
029import static org.nuxeo.mail.MailConstants.CONFIGURATION_MAIL_TRANSPORT_PROTOCOL;
030import static org.nuxeo.mail.MailConstants.DEFAULT_MAIL_JNDI_NAME;
031
032import java.io.PrintStream;
033import java.util.Map;
034import java.util.Objects;
035import java.util.Properties;
036import java.util.function.Function;
037import java.util.stream.Collectors;
038
039import javax.mail.MessagingException;
040import javax.mail.Session;
041import javax.mail.Store;
042import javax.naming.InitialContext;
043import javax.naming.NamingException;
044import javax.validation.constraints.NotNull;
045
046import org.apache.commons.lang3.builder.ToStringBuilder;
047import org.apache.logging.log4j.LogManager;
048import org.apache.logging.log4j.Logger;
049import org.nuxeo.ecm.core.api.NuxeoException;
050import org.nuxeo.runtime.api.Framework;
051
052/**
053 * Builds your {@link javax.mail.Session}.
054 *
055 * @since 11.1
056 */
057public class MailSessionBuilder {
058
059    private static final Logger log = LogManager.getLogger(MailSessionBuilder.class);
060
061    /**
062     * Creates a {@link FromBuilder builder} looking for a jndi {@link Session} and falling back on
063     * {@link Framework#getProperties()} if not found.
064     */
065    public static FromBuilder fromNuxeoConf() {
066        Properties frameworkProperties = Framework.getProperties();
067        Properties properties = frameworkProperties.stringPropertyNames() // no other clean api
068                                                   .stream()
069                                                   .filter(key -> key.startsWith(CONFIGURATION_MAIL_PREFIX))
070                                                   .collect(Collectors.toMap(Function.identity(),
071                                                           frameworkProperties::getProperty, //
072                                                           (v1, v2) -> v2, // should't happen
073                                                           Properties::new));
074        String jndiSessionName = Framework.getProperty(CONFIGURATION_JNDI_JAVA_MAIL, DEFAULT_MAIL_JNDI_NAME);
075        return fromJndi(jndiSessionName).fallbackOn(properties);
076    }
077
078    /**
079     * Creates a {@link FromJndi builder} looking for a session in jndi.
080     */
081    public static FromJndi fromJndi(String jndiSessionName) {
082        return new FromJndi(jndiSessionName);
083    }
084
085    /**
086     * Creates a {@link FromProperties builder} instantiating a new session.
087     */
088    public static FromProperties fromProperties(Properties properties) {
089        return new FromProperties(properties);
090    }
091
092    public static class FromJndi extends AbstractFrom<FromJndi> {
093
094        protected String jndiSessionName;
095
096        private FromJndi(String jndiSessionName) {
097            this.jndiSessionName = Objects.requireNonNull(jndiSessionName, "jndi Mail Session name is required");
098        }
099
100        @Override
101        protected Session retrieveSession() {
102            try {
103                log.debug("Lookup for javax.mail.Session with jndi name: {}", jndiSessionName);
104                return (Session) new InitialContext().lookup(jndiSessionName);
105            } catch (NamingException e) {
106                throw new NuxeoException(e);
107            }
108        }
109
110        public FromProperties fallbackOn(Properties properties) {
111            return new FromProperties(properties) {
112
113                @Override
114                protected Session retrieveSession() {
115                    try {
116                        return FromJndi.this.retrieveSession();
117                    } catch (NuxeoException e) {
118                        log.debug("Lookup failed for javax.mail.Session with jndi name: {}, fallback on properties",
119                                jndiSessionName, e);
120                        return super.retrieveSession();
121                    }
122                }
123
124                @Override
125                public String toString() {
126                    return new ToStringBuilder(this).appendToString(FromJndi.this.toString())
127                                                    .appendToString(super.toString())
128                                                    .build();
129                }
130            };
131        }
132
133        @Override
134        public String toString() {
135            return new ToStringBuilder(this).append("jndiSessionName", jndiSessionName).toString();
136        }
137
138    }
139
140    public static class FromProperties extends AbstractFrom<FromProperties> {
141
142        protected Properties properties;
143
144        private FromProperties(Properties properties) {
145            this.properties = properties;
146            // remove the debug properties due to javax.mail.Session initialization which prints to the console
147            this.debug = properties != null
148                    && Boolean.parseBoolean((String) properties.remove(CONFIGURATION_MAIL_DEBUG));
149        }
150
151        @Override
152        protected Session retrieveSession() {
153            log.debug("Lookup session from properties: {}", properties);
154            return Session.getInstance(properties, new MailAuthenticator(properties));
155        }
156
157        @Override
158        public String toString() {
159            ToStringBuilder builder = new ToStringBuilder(this);
160            var propertiesAsMap = properties.entrySet()
161                                            .stream()
162                                            .filter(not(s -> s.getKey().toString().contains("password")))
163                                            .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
164            builder.append("properties", propertiesAsMap);
165            return builder.toString();
166        }
167    }
168
169    @SuppressWarnings("rawtypes")
170    protected abstract static class AbstractFrom<F extends AbstractFrom> implements FromBuilder {
171
172        protected boolean debug;
173
174        /**
175         * Enables debug mode for built session.
176         *
177         * @implNote {@code mail.debug} in nuxeo.conf takes precedence if it's true.
178         * @return this builder
179         */
180        @SuppressWarnings("unchecked")
181        public F debug() {
182            this.debug = true;
183            return (F) this;
184        }
185
186        public Session build() {
187            log.info("Build a javax.mail.Session from builder: {}", this);
188            if (!debug) {
189                // check nuxeo.conf - same configuration key
190                log.trace("Mail log debug enabled by nuxeo.conf");
191                debug = Boolean.parseBoolean(Framework.getProperty(CONFIGURATION_MAIL_DEBUG, FALSE.toString()));
192            }
193            var session = retrieveSession();
194            if (debug) {
195                session.setDebugOut(new PrintStream(new MailLogOutputStream()));
196                session.setDebug(true);
197            }
198            // set proper protocol for rfc822
199            String protocol = session.getProperty(CONFIGURATION_MAIL_TRANSPORT_PROTOCOL);
200            if (isNotEmpty(protocol)) {
201                session.setProtocolForAddress("rfc822", protocol);
202            }
203            return session;
204        }
205
206        protected abstract Session retrieveSession();
207
208        @Override
209        public Store buildAndConnect() {
210            try {
211                var session = build();
212                var store = session.getStore();
213                // handle backward compatibility for MailCoreHelper
214                // by default user/password are read from authenticator configured through jndi Resource or Properties
215                var user = session.getProperty("user");
216                var password = session.getProperty("password");
217                store.connect(user, password); // accept nulls
218                return store;
219            } catch (MessagingException e) {
220                throw new NuxeoException("Unable to build/connect javax.mail.Store", e);
221            }
222        }
223    }
224
225    public interface FromBuilder {
226
227        @NotNull
228        Session build();
229
230        @NotNull
231        Store buildAndConnect();
232    }
233}