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}