5353import java .util .HashMap ;
5454import java .util .List ;
5555import java .util .Map ;
56+ import java .util .Properties ;
5657import java .util .StringTokenizer ;
58+ import java .util .concurrent .CopyOnWriteArrayList ;
5759
5860/**
5961 * Provides static functions for help with sending email. Supports SMTP and Microsoft Graph transport providers.
@@ -65,11 +67,15 @@ public class MailHelper
6567 private static final Logger _log = LogHelper .getLogger (MailHelper .class , "Errors sending and configuring email" );
6668
6769 // Transport providers
68- private static final SmtpTransportProvider _smtpProvider = new SmtpTransportProvider ();
69- private static final List <EmailTransportProvider > _providers = new ArrayList <>(List .of (_smtpProvider ));
70+ private static final List <EmailTransportProvider > _providers = new CopyOnWriteArrayList <>();
71+
72+ // A neutral session for building MIME messages. Used when the active provider doesn't need session-based transport
73+ // config (e.g. Microsoft Graph) or when no provider is configured. Message assembly (headers, body, attachments)
74+ // doesn't depend on any transport-specific session state.
75+ private static final Session DEFAULT_SESSION = Session .getInstance (new Properties ());
7076
7177 // Active provider (set during initialization)
72- private static EmailTransportProvider _activeProvider = null ;
78+ private static volatile EmailTransportProvider _activeProvider = null ;
7379
7480 // Configuration conflict flag
7581 private static boolean _configurationConflict = false ;
@@ -121,13 +127,24 @@ public static EmailTransportProvider getActiveProvider()
121127 return _activeProvider ;
122128 }
123129
130+ /**
131+ * Directly set the active transport provider, bypassing the normal configuration-driven selection in
132+ * {@link #loadActiveProvider()}. Intended for tools that need to temporarily redirect all outgoing email, such as
133+ * the Dumbster mail recorder, which installs its own {@link SmtpTransportProvider} pointed at a local capture
134+ * server. Callers should save the previous provider (via {@link #getActiveProvider()}) and restore it when done.
135+ */
136+ public static void setActiveProvider (@ Nullable EmailTransportProvider provider )
137+ {
138+ _activeProvider = provider ;
139+ }
140+
124141 public static boolean hasActiveProvider ()
125142 {
126143 return null != _activeProvider ;
127144 }
128145
129146 /**
130- * Registers an optional transport provider. Must be called during module {@code init()} so that
147+ * Registers a transport provider. Must be called during module {@code init()} so that
131148 * all providers are in place before {@link #init()} calls {@link #loadActiveProvider()}.
132149 */
133150 public static void registerProvider (EmailTransportProvider provider )
@@ -140,31 +157,37 @@ public static void init()
140157 _activeProvider = loadActiveProvider ();
141158 }
142159
143- public static void setSmtpSession (Session session )
160+ /**
161+ * @return the {@link Session} to associate with newly created messages, supplied by the active transport provider
162+ * (which decides what session state, if any, a message needs to carry to be delivered). Falls back to a neutral
163+ * session when no provider is configured. Provider-agnostic: callers should not assume this is an SMTP session.
164+ */
165+ @ NotNull
166+ public static Session getSession ()
144167 {
145- _smtpProvider . setSession ( session ) ;
168+ return null != _activeProvider ? _activeProvider . getSession () : DEFAULT_SESSION ;
146169 }
147170
148171 /**
149- * Returns the SMTP session for creating messages
172+ * @return a neutral session suitable for assembling MIME messages that don't need transport-specific session state.
150173 */
151- @ Nullable
152- public static Session getSmtpSession ()
174+ @ NotNull
175+ static Session getDefaultSession ()
153176 {
154- return _smtpProvider . getSession () ;
177+ return DEFAULT_SESSION ;
155178 }
156179
157180 /**
158181 * Creates a blank email message. Caller must set all fields before sending.
159182 */
160183 public static ViewMessage createMessage ()
161184 {
162- return new ViewMessage (getSmtpSession ());
185+ return new ViewMessage (getSession ());
163186 }
164187
165188 public static MultipartMessage createMultipartMessage ()
166189 {
167- return new MultipartMessage (getSmtpSession ());
190+ return new MultipartMessage (getSession ());
168191 }
169192
170193 /**
@@ -199,6 +222,21 @@ public static Address[] createAddressArray(String s) throws AddressException
199222 return addresses .toArray (new Address [0 ]);
200223 }
201224
225+ /**
226+ * Builds the "no email transport configured" message from the hints of the currently registered providers, so that
227+ * an undeployed provider (e.g. Microsoft Graph, when its module isn't present) is never mentioned.
228+ */
229+ private static String noTransportConfiguredMessage ()
230+ {
231+ List <String > hints = _providers .stream ()
232+ .map (EmailTransportProvider ::getConfigurationHint )
233+ .toList ();
234+ if (hints .isEmpty ())
235+ return "No email transport configured and no transport providers are registered." ;
236+ String choices = StringUtilsLabKey .joinWithConjunction (hints , "or" );
237+ return "No email transport configured. Please configure " + choices + " settings." ;
238+ }
239+
202240 /**
203241 * Sends an email message using the configured transport provider. This method logs
204242 * exceptions before throwing them to the caller. The caller should avoid double-logging
@@ -222,9 +260,7 @@ public static void send(Message m, @Nullable User user, Container c)
222260 // Check if any provider is configured
223261 if (_activeProvider == null )
224262 {
225- throw new ConfigurationException (
226- "No email transport configured. Please configure either SMTP (mail.smtp.*) " +
227- "or Microsoft Graph (mail.graph.*) settings." );
263+ throw new ConfigurationException (noTransportConfiguredMessage ());
228264 }
229265
230266 // Send via the active provider
0 commit comments